Version 0.21 (#1935)

* Integrate with httpcore 0.14

* Fix pool timeout test

* Add request extensions to API

* Add certificate and connection info to client, using 'trace' extension

* Fix test_pool_timeout flakiness
This commit is contained in:
Tom Christie 2021-11-15 14:30:54 +00:00 committed by GitHub
parent c531263f42
commit 61188feeae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 218 additions and 117 deletions

View File

@ -1,3 +1,3 @@
__title__ = "httpx"
__description__ = "A next generation HTTP client, for Python 3."
__version__ = "0.20.0"
__version__ = "0.21.0"

View File

@ -323,6 +323,7 @@ class BaseClient:
headers: HeaderTypes = None,
cookies: CookieTypes = None,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Request:
"""
Build and return a request instance.
@ -339,9 +340,14 @@ class BaseClient:
headers = self._merge_headers(headers)
cookies = self._merge_cookies(cookies)
params = self._merge_queryparams(params)
timeout = (
self.timeout if isinstance(timeout, UseClientDefault) else Timeout(timeout)
)
extensions = {} if extensions is None else extensions
if "timeout" not in extensions:
timeout = (
self.timeout
if isinstance(timeout, UseClientDefault)
else Timeout(timeout)
)
extensions["timeout"] = timeout.as_dict()
return Request(
method,
url,
@ -352,7 +358,7 @@ class BaseClient:
params=params,
headers=headers,
cookies=cookies,
extensions={"timeout": timeout.as_dict()},
extensions=extensions,
)
def _merge_url(self, url: URLTypes) -> URL:
@ -459,7 +465,12 @@ class BaseClient:
stream = self._redirect_stream(request, method)
cookies = Cookies(self.cookies)
return Request(
method=method, url=url, headers=headers, cookies=cookies, stream=stream
method=method,
url=url,
headers=headers,
cookies=cookies,
stream=stream,
extensions=request.extensions,
)
def _redirect_method(self, request: Request, response: Response) -> str:
@ -749,6 +760,7 @@ class Client(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Build and send a request.
@ -785,6 +797,7 @@ class Client(BaseClient):
headers=headers,
cookies=cookies,
timeout=timeout,
extensions=extensions,
)
return self.send(request, auth=auth, follow_redirects=follow_redirects)
@ -804,6 +817,7 @@ class Client(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> typing.Iterator[Response]:
"""
Alternative to `httpx.request()` that streams the response body
@ -826,6 +840,7 @@ class Client(BaseClient):
headers=headers,
cookies=cookies,
timeout=timeout,
extensions=extensions,
)
response = self.send(
request=request,
@ -1000,6 +1015,7 @@ class Client(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send a `GET` request.
@ -1015,6 +1031,7 @@ class Client(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
def options(
@ -1027,6 +1044,7 @@ class Client(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send an `OPTIONS` request.
@ -1042,6 +1060,7 @@ class Client(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
def head(
@ -1054,6 +1073,7 @@ class Client(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send a `HEAD` request.
@ -1069,6 +1089,7 @@ class Client(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
def post(
@ -1085,6 +1106,7 @@ class Client(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send a `POST` request.
@ -1104,6 +1126,7 @@ class Client(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
def put(
@ -1120,6 +1143,7 @@ class Client(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send a `PUT` request.
@ -1139,6 +1163,7 @@ class Client(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
def patch(
@ -1155,6 +1180,7 @@ class Client(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send a `PATCH` request.
@ -1174,6 +1200,7 @@ class Client(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
def delete(
@ -1186,6 +1213,7 @@ class Client(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send a `DELETE` request.
@ -1201,6 +1229,7 @@ class Client(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
def close(self) -> None:
@ -1450,6 +1479,7 @@ class AsyncClient(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Build and send a request.
@ -1478,6 +1508,7 @@ class AsyncClient(BaseClient):
headers=headers,
cookies=cookies,
timeout=timeout,
extensions=extensions,
)
return await self.send(request, auth=auth, follow_redirects=follow_redirects)
@ -1497,6 +1528,7 @@ class AsyncClient(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> typing.AsyncIterator[Response]:
"""
Alternative to `httpx.request()` that streams the response body
@ -1519,6 +1551,7 @@ class AsyncClient(BaseClient):
headers=headers,
cookies=cookies,
timeout=timeout,
extensions=extensions,
)
response = await self.send(
request=request,
@ -1693,6 +1726,7 @@ class AsyncClient(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send a `GET` request.
@ -1708,6 +1742,7 @@ class AsyncClient(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
async def options(
@ -1720,6 +1755,7 @@ class AsyncClient(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send an `OPTIONS` request.
@ -1735,6 +1771,7 @@ class AsyncClient(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
async def head(
@ -1747,6 +1784,7 @@ class AsyncClient(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send a `HEAD` request.
@ -1762,6 +1800,7 @@ class AsyncClient(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
async def post(
@ -1778,6 +1817,7 @@ class AsyncClient(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send a `POST` request.
@ -1797,6 +1837,7 @@ class AsyncClient(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
async def put(
@ -1813,6 +1854,7 @@ class AsyncClient(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send a `PUT` request.
@ -1832,6 +1874,7 @@ class AsyncClient(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
async def patch(
@ -1848,6 +1891,7 @@ class AsyncClient(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send a `PATCH` request.
@ -1867,6 +1911,7 @@ class AsyncClient(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
async def delete(
@ -1879,6 +1924,7 @@ class AsyncClient(BaseClient):
auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
extensions: dict = None,
) -> Response:
"""
Send a `DELETE` request.
@ -1894,6 +1940,7 @@ class AsyncClient(BaseClient):
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
)
async def aclose(self) -> None:

View File

@ -4,6 +4,7 @@ import sys
import typing
import click
import httpcore
import pygments.lexers
import pygments.util
import rich.console
@ -12,7 +13,8 @@ import rich.syntax
from ._client import Client
from ._exceptions import RequestError
from ._models import Request, Response
from ._models import Response
from ._status_codes import codes
def print_help() -> None:
@ -102,29 +104,38 @@ def get_lexer_for_response(response: Response) -> str:
return "" # pragma: nocover
def format_request_headers(request: Request, http2: bool = False) -> str:
def format_request_headers(request: httpcore.Request, http2: bool = False) -> str:
version = "HTTP/2" if http2 else "HTTP/1.1"
headers = [
(name.lower() if http2 else name, value) for name, value in request.headers.raw
(name.lower() if http2 else name, value) for name, value in request.headers
]
target = request.url.raw[-1].decode("ascii")
lines = [f"{request.method} {target} {version}"] + [
method = request.method.decode("ascii")
target = request.url.target.decode("ascii")
lines = [f"{method} {target} {version}"] + [
f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers
]
return "\n".join(lines)
def format_response_headers(response: Response) -> str:
lines = [
f"{response.http_version} {response.status_code} {response.reason_phrase}"
] + [
f"{name.decode('ascii')}: {value.decode('ascii')}"
for name, value in response.headers.raw
def format_response_headers(
http_version: bytes,
status: int,
reason_phrase: typing.Optional[bytes],
headers: typing.List[typing.Tuple[bytes, bytes]],
) -> str:
version = http_version.decode("ascii")
reason = (
codes.get_reason_phrase(status)
if reason_phrase is None
else reason_phrase.decode("ascii")
)
lines = [f"{version} {status} {reason}"] + [
f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers
]
return "\n".join(lines)
def print_request_headers(request: Request, http2: bool = False) -> None:
def print_request_headers(request: httpcore.Request, http2: bool = False) -> None:
console = rich.console.Console()
http_text = format_request_headers(request, http2=http2)
syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True)
@ -133,26 +144,20 @@ def print_request_headers(request: Request, http2: bool = False) -> None:
console.print(syntax)
def print_response_headers(response: Response) -> None:
def print_response_headers(
http_version: bytes,
status: int,
reason_phrase: typing.Optional[bytes],
headers: typing.List[typing.Tuple[bytes, bytes]],
) -> None:
console = rich.console.Console()
http_text = format_response_headers(response)
http_text = format_response_headers(http_version, status, reason_phrase, headers)
syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True)
console.print(syntax)
def print_delimiter() -> None:
console = rich.console.Console()
syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True)
console.print(syntax)
def print_redirects(response: Response) -> None:
if response.has_redirect_location:
response.read()
print_response_headers(response)
print_response(response)
def print_response(response: Response) -> None:
console = rich.console.Console()
lexer_name = get_lexer_for_response(response)
@ -171,6 +176,61 @@ def print_response(response: Response) -> None:
console.print(response.text)
def format_certificate(cert: dict) -> str: # pragma: nocover
lines = []
for key, value in cert.items():
if isinstance(value, (list, tuple)):
lines.append(f"* {key}:")
for item in value:
if key in ("subject", "issuer"):
for sub_item in item:
lines.append(f"* {sub_item[0]}: {sub_item[1]!r}")
elif isinstance(item, tuple) and len(item) == 2:
lines.append(f"* {item[0]}: {item[1]!r}")
else:
lines.append(f"* {item!r}")
else:
lines.append(f"* {key}: {value!r}")
return "\n".join(lines)
def trace(name: str, info: dict, verbose: bool = False) -> None:
console = rich.console.Console()
if name == "connection.connect_tcp.started" and verbose:
host = info["host"]
console.print(f"* Connecting to {host!r}")
elif name == "connection.connect_tcp.complete" and verbose:
stream = info["return_value"]
server_addr = stream.get_extra_info("server_addr")
console.print(f"* Connected to {server_addr[0]!r} on port {server_addr[1]}")
elif name == "connection.start_tls.complete" and verbose: # pragma: nocover
stream = info["return_value"]
ssl_object = stream.get_extra_info("ssl_object")
version = ssl_object.version()
cipher = ssl_object.cipher()
server_cert = ssl_object.getpeercert()
alpn = ssl_object.selected_alpn_protocol()
console.print(f"* SSL established using {version!r} / {cipher[0]!r}")
console.print(f"* Selected ALPN protocol: {alpn!r}")
if server_cert:
console.print("* Server certificate:")
console.print(format_certificate(server_cert))
elif name == "http11.send_request_headers.started" and verbose:
request = info["request"]
print_request_headers(request, http2=False)
elif name == "http2.send_request_headers.started" and verbose: # pragma: nocover
request = info["request"]
print_request_headers(request, http2=True)
elif name == "http11.receive_response_headers.complete":
http_version, status, reason_phrase, headers = info["return_value"]
print_response_headers(http_version, status, reason_phrase, headers)
elif name == "http2.receive_response_headers.complete": # pragma: nocover
status, headers = info["return_value"]
http_version = b"HTTP/2"
reason_phrase = None
print_response_headers(http_version, status, reason_phrase, headers)
def download_response(response: Response, download: typing.BinaryIO) -> None:
console = rich.console.Console()
syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True)
@ -397,19 +457,12 @@ def main(
if not method:
method = "POST" if content or data or files or json else "GET"
event_hooks: typing.Dict[str, typing.List[typing.Callable]] = {}
if verbose:
event_hooks["request"] = [functools.partial(print_request_headers, http2=http2)]
if follow_redirects:
event_hooks["response"] = [print_redirects]
try:
with Client(
proxies=proxies,
timeout=timeout,
verify=verify,
http2=http2,
event_hooks=event_hooks,
) as client:
with client.stream(
method,
@ -423,20 +476,18 @@ def main(
cookies=dict(cookies),
auth=auth,
follow_redirects=follow_redirects,
extensions={"trace": functools.partial(trace, verbose=verbose)},
) as response:
print_response_headers(response)
if download is not None:
download_response(response, download)
else:
response.read()
if response.content:
print_delimiter()
print_response(response)
except RequestError as exc:
console = rich.console.Console()
console.print(f"{type(exc).__name__}: {exc}")
console.print(f"[red]{type(exc).__name__}[/red]: {str(exc)}")
sys.exit(1)
sys.exit(0 if response.is_success else 1)

View File

@ -6,7 +6,6 @@ The following additional keyword arguments are currently supported by httpcore..
* uds: str
* local_address: str
* retries: int
* backend: str ("auto", "asyncio", "trio", "curio", "anyio", "sync")
Example usages...
@ -32,7 +31,6 @@ import httpcore
from .._config import DEFAULT_LIMITS, Limits, Proxy, create_ssl_context
from .._exceptions import (
CloseError,
ConnectError,
ConnectTimeout,
LocalProtocolError,
@ -89,7 +87,6 @@ HTTPCORE_EXC_MAP = {
httpcore.ConnectError: ConnectError,
httpcore.ReadError: ReadError,
httpcore.WriteError: WriteError,
httpcore.CloseError: CloseError,
httpcore.ProxyError: ProxyError,
httpcore.UnsupportedProtocol: UnsupportedProtocol,
httpcore.ProtocolError: ProtocolError,
@ -99,7 +96,7 @@ HTTPCORE_EXC_MAP = {
class ResponseStream(SyncByteStream):
def __init__(self, httpcore_stream: httpcore.SyncByteStream):
def __init__(self, httpcore_stream: typing.Iterable[bytes]):
self._httpcore_stream = httpcore_stream
def __iter__(self) -> typing.Iterator[bytes]:
@ -108,8 +105,8 @@ class ResponseStream(SyncByteStream):
yield part
def close(self) -> None:
with map_httpcore_exceptions():
self._httpcore_stream.close()
if hasattr(self._httpcore_stream, "close"):
self._httpcore_stream.close() # type: ignore
class HTTPTransport(BaseTransport):
@ -125,12 +122,11 @@ class HTTPTransport(BaseTransport):
uds: str = None,
local_address: str = None,
retries: int = 0,
backend: str = "sync",
) -> None:
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
if proxy is None:
self._pool = httpcore.SyncConnectionPool(
self._pool = httpcore.ConnectionPool(
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
@ -140,18 +136,20 @@ class HTTPTransport(BaseTransport):
uds=uds,
local_address=local_address,
retries=retries,
backend=backend,
)
else:
self._pool = httpcore.SyncHTTPProxy(
proxy_url=proxy.url.raw,
self._pool = httpcore.HTTPProxy(
proxy_url=httpcore.URL(
scheme=proxy.url.raw_scheme,
host=proxy.url.raw_host,
port=proxy.url.port,
target=proxy.url.raw_path,
),
proxy_headers=proxy.headers.raw,
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=limits.keepalive_expiry,
http2=http2,
backend=backend,
)
def __enter__(self: T) -> T: # Use generics for subclass support.
@ -173,19 +171,28 @@ class HTTPTransport(BaseTransport):
) -> Response:
assert isinstance(request.stream, SyncByteStream)
req = httpcore.Request(
method=request.method,
url=httpcore.URL(
scheme=request.url.raw_scheme,
host=request.url.raw_host,
port=request.url.port,
target=request.url.raw_path,
),
headers=request.headers.raw,
content=request.stream,
extensions=request.extensions,
)
with map_httpcore_exceptions():
status_code, headers, byte_stream, extensions = self._pool.handle_request(
method=request.method.encode("ascii"),
url=request.url.raw,
headers=request.headers.raw,
stream=httpcore.IteratorByteStream(iter(request.stream)),
extensions=request.extensions,
)
resp = self._pool.handle_request(req)
stream = ResponseStream(byte_stream)
assert isinstance(resp.stream, typing.Iterable)
return Response(
status_code, headers=headers, stream=stream, extensions=extensions
status_code=resp.status,
headers=resp.headers,
stream=ResponseStream(resp.stream),
extensions=resp.extensions,
)
def close(self) -> None:
@ -193,7 +200,7 @@ class HTTPTransport(BaseTransport):
class AsyncResponseStream(AsyncByteStream):
def __init__(self, httpcore_stream: httpcore.AsyncByteStream):
def __init__(self, httpcore_stream: typing.AsyncIterable[bytes]):
self._httpcore_stream = httpcore_stream
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
@ -202,8 +209,8 @@ class AsyncResponseStream(AsyncByteStream):
yield part
async def aclose(self) -> None:
with map_httpcore_exceptions():
await self._httpcore_stream.aclose()
if hasattr(self._httpcore_stream, "aclose"):
await self._httpcore_stream.aclose() # type: ignore
class AsyncHTTPTransport(AsyncBaseTransport):
@ -219,7 +226,6 @@ class AsyncHTTPTransport(AsyncBaseTransport):
uds: str = None,
local_address: str = None,
retries: int = 0,
backend: str = "auto",
) -> None:
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
@ -234,18 +240,20 @@ class AsyncHTTPTransport(AsyncBaseTransport):
uds=uds,
local_address=local_address,
retries=retries,
backend=backend,
)
else:
self._pool = httpcore.AsyncHTTPProxy(
proxy_url=proxy.url.raw,
proxy_url=httpcore.URL(
scheme=proxy.url.raw_scheme,
host=proxy.url.raw_host,
port=proxy.url.port,
target=proxy.url.raw_path,
),
proxy_headers=proxy.headers.raw,
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=limits.keepalive_expiry,
http2=http2,
backend=backend,
)
async def __aenter__(self: A) -> A: # Use generics for subclass support.
@ -267,24 +275,28 @@ class AsyncHTTPTransport(AsyncBaseTransport):
) -> Response:
assert isinstance(request.stream, AsyncByteStream)
req = httpcore.Request(
method=request.method,
url=httpcore.URL(
scheme=request.url.raw_scheme,
host=request.url.raw_host,
port=request.url.port,
target=request.url.raw_path,
),
headers=request.headers.raw,
content=request.stream,
extensions=request.extensions,
)
with map_httpcore_exceptions():
(
status_code,
headers,
byte_stream,
extensions,
) = await self._pool.handle_async_request(
method=request.method.encode("ascii"),
url=request.url.raw,
headers=request.headers.raw,
stream=httpcore.AsyncIteratorByteStream(request.stream.__aiter__()),
extensions=request.extensions,
)
resp = await self._pool.handle_async_request(req)
stream = AsyncResponseStream(byte_stream)
assert isinstance(resp.stream, typing.AsyncIterable)
return Response(
status_code, headers=headers, stream=stream, extensions=extensions
status_code=resp.status,
headers=resp.headers,
stream=AsyncResponseStream(resp.stream),
extensions=resp.extensions,
)
async def aclose(self) -> None:

View File

@ -60,7 +60,7 @@ setup(
"charset_normalizer",
"sniffio",
"rfc3986[idna2008]>=1.3,<2",
"httpcore>=0.13.3,<0.14.0",
"httpcore>=0.14.0,<0.15.0",
"async_generator; python_version < '3.7'"
],
extras_require={

View File

@ -10,11 +10,8 @@ def url_to_origin(url: str):
Given a URL string, return the origin in the raw tuple format that
`httpcore` uses for it's representation.
"""
DEFAULT_PORTS = {b"http": 80, b"https": 443}
scheme, host, explicit_port = httpx.URL(url).raw[:3]
default_port = DEFAULT_PORTS[scheme]
port = default_port if explicit_port is None else explicit_port
return scheme, host, port
scheme, host, port = httpx.URL(url).raw[:3]
return httpcore.URL(scheme=scheme, host=host, port=port, target="/")
@pytest.mark.parametrize(
@ -44,8 +41,8 @@ def test_proxies_parameter(proxies, expected_proxies):
assert pattern in client._mounts
proxy = client._mounts[pattern]
assert isinstance(proxy, httpx.HTTPTransport)
assert isinstance(proxy._pool, httpcore.SyncHTTPProxy)
assert proxy._pool.proxy_origin == url_to_origin(url)
assert isinstance(proxy._pool, httpcore.HTTPProxy)
assert proxy._pool._proxy_url == url_to_origin(url)
assert len(expected_proxies) == len(client._mounts)
@ -117,8 +114,8 @@ def test_transport_for_request(url, proxies, expected):
assert transport is client._transport
else:
assert isinstance(transport, httpx.HTTPTransport)
assert isinstance(transport._pool, httpcore.SyncHTTPProxy)
assert transport._pool.proxy_origin == url_to_origin(expected)
assert isinstance(transport._pool, httpcore.HTTPProxy)
assert transport._pool._proxy_url == url_to_origin(expected)
@pytest.mark.asyncio
@ -253,7 +250,7 @@ def test_proxies_environ(monkeypatch, client_class, url, env, expected):
if expected is None:
assert transport == client._transport
else:
assert transport._pool.proxy_origin == url_to_origin(expected)
assert transport._pool._proxy_url == url_to_origin(expected)
@pytest.mark.parametrize(

View File

@ -18,6 +18,7 @@ def test_httpcore_all_exceptions_mapped() -> None:
if isinstance(value, type)
and issubclass(value, Exception)
and value not in HTTPCORE_EXC_MAP
and value is not httpcore.ConnectionNotAvailable
]
if not_mapped: # pragma: nocover
@ -39,33 +40,21 @@ def test_httpcore_exception_mapping(server) -> None:
def close(self):
pass
class CloseFailedStream:
def __iter__(self):
yield b""
def close(self):
raise httpcore.CloseError()
with mock.patch(
"httpcore.SyncConnectionPool.handle_request", side_effect=connect_failed
"httpcore.ConnectionPool.handle_request", side_effect=connect_failed
):
with pytest.raises(httpx.ConnectError):
httpx.get(server.url)
with mock.patch(
"httpcore.SyncConnectionPool.handle_request",
return_value=(200, [], TimeoutStream(), {}),
"httpcore.ConnectionPool.handle_request",
return_value=httpcore.Response(
200, headers=[], content=TimeoutStream(), extensions={}
),
):
with pytest.raises(httpx.ReadTimeout):
httpx.get(server.url)
with mock.patch(
"httpcore.SyncConnectionPool.handle_request",
return_value=(200, [], CloseFailedStream(), {}),
):
with pytest.raises(httpx.CloseError):
httpx.get(server.url)
def test_httpx_exceptions_exposed() -> None:
"""

View File

@ -62,6 +62,7 @@ def test_redirects(server):
"server: uvicorn",
"location: /",
"Transfer-Encoding: chunked",
"",
]
@ -106,6 +107,8 @@ def test_verbose(server):
result = runner.invoke(httpx.main, [url, "-v"])
assert result.exit_code == 0
assert remove_date_header(splitlines(result.output)) == [
"* Connecting to '127.0.0.1'",
"* Connected to '127.0.0.1' on port 8000",
"GET / HTTP/1.1",
f"Host: {server.url.netloc.decode('ascii')}",
"Accept: */*",
@ -129,6 +132,8 @@ def test_auth(server):
print(result.output)
assert result.exit_code == 0
assert remove_date_header(splitlines(result.output)) == [
"* Connecting to '127.0.0.1'",
"* Connected to '127.0.0.1' on port 8000",
"GET / HTTP/1.1",
f"Host: {server.url.netloc.decode('ascii')}",
"Accept: */*",

View File

@ -39,6 +39,6 @@ async def test_pool_timeout(server):
timeout = httpx.Timeout(None, pool=1e-4)
async with httpx.AsyncClient(limits=limits, timeout=timeout) as client:
async with client.stream("GET", server.url):
with pytest.raises(httpx.PoolTimeout):
await client.get("http://localhost:8000/")
with pytest.raises(httpx.PoolTimeout):
async with client.stream("GET", server.url):
await client.get(server.url)