Skip to content

Commit ef50c15

Browse files
authored
Merge branch 'master' into feat/trio-backend
2 parents 6af585a + 8155352 commit ef50c15

File tree

22 files changed

+621
-73
lines changed

22 files changed

+621
-73
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Plus all the standard features of `requests`...
5656
* Keep-Alive & Connection Pooling
5757
* Sessions with Cookie Persistence
5858
* Browser-style SSL Verification
59-
* Basic/Digest Authentication *(Digest is still TODO)*
59+
* Basic/Digest Authentication
6060
* Elegant Key/Value Cookies
6161
* Automatic Decompression
6262
* Automatic Content Decoding

docs/async.md

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,30 @@ async client for sending outgoing HTTP requests.
1515
To make asynchronous requests, you'll need an `AsyncClient`.
1616

1717
```python
18-
>>> client = httpx.AsyncClient()
19-
>>> r = await client.get('https://www.example.com/')
18+
>>> async with httpx.AsyncClient() as client:
19+
>>> r = await client.get('https://www.example.com/')
20+
>>> r
21+
<Response [200 OK]>
2022
```
2123

24+
!!! tip
25+
Use [IPython](https://ipython.readthedocs.io/en/stable/) to try this code interactively, as it supports executing `async`/`await` expressions in the console.
26+
27+
!!! note
28+
The `async with` syntax ensures that all active connections are closed on exit.
29+
30+
It is safe to access response content (e.g. `r.text`) both inside and outside the `async with` block, unless you are using response streaming. In that case, you should `.read()`, `.stream()`, or `.close()` the response *inside* the `async with` block.
31+
2232
## API Differences
2333

2434
If you're using streaming responses then there are a few bits of API that
2535
use async methods:
2636

2737
```python
28-
>>> client = httpx.AsyncClient()
29-
>>> r = await client.get('https://www.example.com/', stream=True)
30-
>>> try:
38+
>>> async with httpx.AsyncClient() as client:
39+
>>> r = await client.get('https://www.example.com/', stream=True)
3140
>>> async for chunk in r.stream():
3241
>>> ...
33-
>>> finally:
34-
>>> await r.close()
3542
```
3643

3744
The async response methods are:
@@ -41,15 +48,15 @@ The async response methods are:
4148
* `.raw()`
4249
* `.close()`
4350

44-
If you're making parallel requests, then you'll also need to use an async API:
51+
If you're making [parallel requests](/parallel/), then you'll also need to use an async API:
4552

4653
```python
47-
>>> client = httpx.AsyncClient()
48-
>>> async with client.parallel() as parallel:
49-
>>> pending_one = parallel.get('https://example.com/1')
50-
>>> pending_two = parallel.get('https://example.com/2')
51-
>>> response_one = await pending_one.get_response()
52-
>>> response_two = await pending_two.get_response()
54+
>>> async with httpx.AsyncClient() as client:
55+
>>> async with client.parallel() as parallel:
56+
>>> pending_one = parallel.get('https://example.com/1')
57+
>>> pending_two = parallel.get('https://example.com/2')
58+
>>> response_one = await pending_one.get_response()
59+
>>> response_two = await pending_two.get_response()
5360
```
5461

5562
The async parallel methods are:

docs/quickstart.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,3 +379,25 @@ value to be more or less strict:
379379
```python
380380
>>> httpx.get('https://github.com/', timeout=0.001)
381381
```
382+
383+
## Authentication
384+
385+
HTTPX supports Basic and Digest HTTP authentication.
386+
387+
To provide Basic authentication credentials, pass a 2-tuple of
388+
plaintext `str` or `bytes` objects as the `auth` argument to the request
389+
functions:
390+
391+
```python
392+
>>> httpx.get("https://example.com", auth=("my_user", "password123"))
393+
```
394+
395+
To provide credentials for Digest authentication you'll need to instantiate
396+
a `DigestAuth` object with the plaintext username and password as arguments.
397+
This object can be then passed as the `auth` argument to the request methods
398+
as above:
399+
400+
```python
401+
>>> auth = httpx.DigestAuth("my_user", "password123")
402+
>>> httpx.get("https://example.com", auth=auth)
403+
<Response [200 OK]>

httpx/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from .concurrency.base import (
66
BaseBackgroundManager,
77
BasePoolSemaphore,
8-
BaseStream,
8+
BaseTCPStream,
99
ConcurrencyBackend,
1010
)
1111
from .config import (
@@ -22,6 +22,7 @@
2222
from .dispatch.base import AsyncDispatcher, Dispatcher
2323
from .dispatch.connection import HTTPConnection
2424
from .dispatch.connection_pool import ConnectionPool
25+
from .dispatch.proxy_http import HTTPProxy, HTTPProxyMode
2526
from .exceptions import (
2627
ConnectTimeout,
2728
CookieConflict,
@@ -30,6 +31,7 @@
3031
NotRedirectResponse,
3132
PoolTimeout,
3233
ProtocolError,
34+
ProxyError,
3335
ReadTimeout,
3436
RedirectBodyUnavailable,
3537
RedirectLoop,
@@ -90,6 +92,8 @@
9092
"BasePoolSemaphore",
9193
"BaseBackgroundManager",
9294
"ConnectionPool",
95+
"HTTPProxy",
96+
"HTTPProxyMode",
9397
"ConnectTimeout",
9498
"CookieConflict",
9599
"DecodingError",
@@ -103,11 +107,12 @@
103107
"ResponseClosed",
104108
"ResponseNotRead",
105109
"StreamConsumed",
110+
"ProxyError",
106111
"Timeout",
107112
"TooManyRedirects",
108113
"WriteTimeout",
109114
"AsyncDispatcher",
110-
"BaseStream",
115+
"BaseTCPStream",
111116
"ConcurrencyBackend",
112117
"Dispatcher",
113118
"URL",

httpx/api.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ def request(
2727
auth: AuthTypes = None,
2828
timeout: TimeoutTypes = None,
2929
allow_redirects: bool = True,
30-
# proxies
3130
cert: CertTypes = None,
3231
verify: VerifyTypes = True,
3332
stream: bool = False,

httpx/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ async def head(
334334
cookies: CookieTypes = None,
335335
stream: bool = False,
336336
auth: AuthTypes = None,
337-
allow_redirects: bool = False, #  Note: Differs to usual default.
337+
allow_redirects: bool = False, # NOTE: Differs to usual default.
338338
cert: CertTypes = None,
339339
verify: VerifyTypes = None,
340340
timeout: TimeoutTypes = None,
@@ -784,7 +784,7 @@ def head(
784784
cookies: CookieTypes = None,
785785
stream: bool = False,
786786
auth: AuthTypes = None,
787-
allow_redirects: bool = False, #  Note: Differs to usual default.
787+
allow_redirects: bool = False, # NOTE: Differs to usual default.
788788
cert: CertTypes = None,
789789
verify: VerifyTypes = None,
790790
timeout: TimeoutTypes = None,

httpx/concurrency/asyncio.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,3 @@
1-
"""
2-
The `Stream` class here provides a lightweight layer over
3-
`asyncio.StreamReader` and `asyncio.StreamWriter`.
4-
5-
Similarly `PoolSemaphore` is a lightweight layer over `BoundedSemaphore`.
6-
7-
These classes help encapsulate the timeout logic, make it easier to unit-test
8-
protocols, and help keep the rest of the package more `async`/`await`
9-
based, and less strictly `asyncio`-specific.
10-
"""
111
import asyncio
122
import functools
133
import ssl
@@ -21,7 +11,7 @@
2111
BaseEvent,
2212
BasePoolSemaphore,
2313
BaseQueue,
24-
BaseStream,
14+
BaseTCPStream,
2515
ConcurrencyBackend,
2616
TimeoutFlag,
2717
)
@@ -50,7 +40,7 @@ def _fixed_write(self, data: bytes) -> None: # type: ignore
5040
MonkeyPatch.write = _fixed_write
5141

5242

53-
class Stream(BaseStream):
43+
class TCPStream(BaseTCPStream):
5444
def __init__(
5545
self,
5646
stream_reader: asyncio.StreamReader,
@@ -190,13 +180,13 @@ def loop(self) -> asyncio.AbstractEventLoop:
190180
self._loop = asyncio.new_event_loop()
191181
return self._loop
192182

193-
async def connect(
183+
async def open_tcp_stream(
194184
self,
195185
hostname: str,
196186
port: int,
197187
ssl_context: typing.Optional[ssl.SSLContext],
198188
timeout: TimeoutConfig,
199-
) -> BaseStream:
189+
) -> BaseTCPStream:
200190
try:
201191
stream_reader, stream_writer = await asyncio.wait_for( # type: ignore
202192
asyncio.open_connection(hostname, port, ssl=ssl_context),
@@ -205,25 +195,25 @@ async def connect(
205195
except asyncio.TimeoutError:
206196
raise ConnectTimeout()
207197

208-
return Stream(
198+
return TCPStream(
209199
stream_reader=stream_reader, stream_writer=stream_writer, timeout=timeout
210200
)
211201

212202
async def start_tls(
213203
self,
214-
stream: BaseStream,
204+
stream: BaseTCPStream,
215205
hostname: str,
216206
ssl_context: ssl.SSLContext,
217207
timeout: TimeoutConfig,
218-
) -> BaseStream:
208+
) -> BaseTCPStream:
219209

220210
loop = self.loop
221211
if not hasattr(loop, "start_tls"): # pragma: no cover
222212
raise NotImplementedError(
223213
"asyncio.AbstractEventLoop.start_tls() is only available in Python 3.7+"
224214
)
225215

226-
assert isinstance(stream, Stream)
216+
assert isinstance(stream, TCPStream)
227217

228218
stream_reader = asyncio.StreamReader()
229219
protocol = asyncio.StreamReaderProtocol(stream_reader)

httpx/concurrency/base.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ def set_write_timeouts(self) -> None:
3737
self.raise_on_write_timeout = True
3838

3939

40-
class BaseStream:
40+
class BaseTCPStream:
4141
"""
42-
A stream with read/write operations. Abstracts away any asyncio-specific
42+
A TCP stream with read/write operations. Abstracts away any asyncio-specific
4343
interfaces into a more generic base class, that we can use with alternate
4444
backends, or for stand-alone test cases.
4545
"""
@@ -110,22 +110,22 @@ def release(self) -> None:
110110

111111

112112
class ConcurrencyBackend:
113-
async def connect(
113+
async def open_tcp_stream(
114114
self,
115115
hostname: str,
116116
port: int,
117117
ssl_context: typing.Optional[ssl.SSLContext],
118118
timeout: TimeoutConfig,
119-
) -> BaseStream:
119+
) -> BaseTCPStream:
120120
raise NotImplementedError() # pragma: no cover
121121

122122
async def start_tls(
123123
self,
124-
stream: BaseStream,
124+
stream: BaseTCPStream,
125125
hostname: str,
126126
ssl_context: ssl.SSLContext,
127127
timeout: TimeoutConfig,
128-
) -> BaseStream:
128+
) -> BaseTCPStream:
129129
raise NotImplementedError() # pragma: no cover
130130

131131
def get_semaphore(self, limits: PoolLimits) -> BasePoolSemaphore:

httpx/dispatch/connection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ async def connect(
8585
on_release = functools.partial(self.release_func, self)
8686

8787
logger.debug(f"start_connect host={host!r} port={port!r} timeout={timeout!r}")
88-
stream = await self.backend.connect(host, port, ssl_context, timeout)
88+
stream = await self.backend.open_tcp_stream(host, port, ssl_context, timeout)
8989
http_version = stream.get_http_version()
9090
logger.debug(f"connected http_version={http_version!r}")
9191

httpx/dispatch/connection_pool.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,8 @@ async def send(
128128
return response
129129

130130
async def acquire_connection(self, origin: Origin) -> HTTPConnection:
131-
logger.debug(f"acquire_connection origin={origin!r}")
132-
connection = self.active_connections.pop_by_origin(origin, http2_only=True)
133-
if connection is None:
134-
connection = self.keepalive_connections.pop_by_origin(origin)
135-
136-
if connection is not None and connection.is_connection_dropped():
137-
self.max_connections.release()
138-
connection = None
131+
logger.debug("acquire_connection origin={origin!r}")
132+
connection = self.pop_connection(origin)
139133

140134
if connection is None:
141135
await self.max_connections.acquire()
@@ -179,3 +173,14 @@ async def close(self) -> None:
179173
self.keepalive_connections.clear()
180174
for connection in connections:
181175
await connection.close()
176+
177+
def pop_connection(self, origin: Origin) -> typing.Optional[HTTPConnection]:
178+
connection = self.active_connections.pop_by_origin(origin, http2_only=True)
179+
if connection is None:
180+
connection = self.keepalive_connections.pop_by_origin(origin)
181+
182+
if connection is not None and connection.is_connection_dropped():
183+
self.max_connections.release()
184+
connection = None
185+
186+
return connection

0 commit comments

Comments
 (0)