Skip to content

Commit 80e7e71

Browse files
Merge pull request #16 from thewebscraping/bugs/async-client-is-not-asynchronous
bugs: AsyncClient is not asynchronous #15
2 parents 817ca01 + 7ec14ad commit 80e7e71

File tree

5 files changed

+105
-42
lines changed

5 files changed

+105
-42
lines changed

MANIFEST.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
include LICENSE
22
include README.md
33
include CHANGELOG.md
4-
recursive-include tls_requests docs Makefile *.md *.rst
4+
recursive-include tls_requests docs Makefile *.md

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ test:
1111
rm -rf *.egg-info
1212

1313
test-readme:
14-
python setup.py check --restructuredtext --strict && ([ $$? -eq 0 ] && echo "README.rst and CHANGELOG.md ok") || echo "Invalid markup in README.md or CHANGELOG.md!"
14+
python setup.py check --restructuredtext --strict && ([ $$? -eq 0 ] && echo "README.md and CHANGELOG.md ok") || echo "Invalid markup in README.md or CHANGELOG.md!"
1515

1616
pytest:
1717
python -m pytest tests

docs/advanced/async_client.md

+29-6
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,37 @@ To send asynchronous HTTP requests, use the `AsyncClient`:
2323

2424
```pycon
2525
>>> import asyncio
26-
>>> async def fetch(url):
27-
async with tls_requests.AsyncClient() as client:
28-
r = await client.get(url)
29-
return r
26+
>>> import random
27+
>>> import time
28+
>>> import tls_requests
29+
>>> async def fetch(idx, url):
30+
async with tls_requests.AsyncClient() as client:
31+
rand = random.uniform(0.1, 1.5)
32+
start_time = time.perf_counter()
33+
print("%s: Sleep for %.2f seconds." % (idx, rand))
34+
await asyncio.sleep(rand)
35+
response = await client.get(url)
36+
end_time = time.perf_counter()
37+
print("%s: Took: %.2f" % (idx, (end_time - start_time)))
38+
return response
39+
>>> async def run(urls):
40+
tasks = [asyncio.create_task(fetch(idx, url)) for idx, url in enumerate(urls)]
41+
responses = await asyncio.gather(*tasks)
42+
return responses
3043

31-
>>> r = asyncio.run(fetch("https://httpbin.org/get"))
44+
>>> start_urls = [
45+
'https://httpbin.org/absolute-redirect/1',
46+
'https://httpbin.org/absolute-redirect/2',
47+
'https://httpbin.org/absolute-redirect/3',
48+
'https://httpbin.org/absolute-redirect/4',
49+
'https://httpbin.org/absolute-redirect/5',
50+
]
51+
52+
53+
>>> r = asyncio.run(run(start_urls))
3254
>>> r
33-
<Response [200 OK]>
55+
[<Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>]
56+
3457
```
3558

3659
!!! tip

tls_requests/client.py

+60-34
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,6 @@ def _rebuild_redirect_url(self, request: Request, response: Response) -> URL:
336336
def _send(
337337
self, request: Request, *, history: list = None, start: float = None
338338
) -> Response:
339-
history = history if isinstance(history, list) else []
340339
start = start or time.perf_counter()
341340
config = self.prepare_config(request)
342341
response = Response.from_tls_response(
@@ -483,6 +482,7 @@ def send(
483482
response = self._send(
484483
request,
485484
start=time.perf_counter(),
485+
history=[]
486486
)
487487

488488
if self.hooks.get("response"):
@@ -755,39 +755,6 @@ async def request(
755755
)
756756
return await self.send(request, auth=auth, follow_redirects=follow_redirects)
757757

758-
async def send(
759-
self,
760-
request: Request,
761-
*,
762-
stream: bool = False,
763-
auth: AuthTypes = None,
764-
follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS,
765-
) -> Response:
766-
if self._state == ClientState.CLOSED:
767-
raise RuntimeError("Cannot send a request, as the client has been closed.")
768-
769-
self._state = ClientState.OPENED
770-
for fn in [self.prepare_auth, self.build_hook_request]:
771-
request_ = fn(request, auth or self.auth, follow_redirects)
772-
if isinstance(request_, Request):
773-
request = request_
774-
775-
self.follow_redirects = follow_redirects
776-
response = self._send(
777-
request,
778-
start=time.perf_counter(),
779-
)
780-
781-
if self.hooks.get("response"):
782-
response_ = self.build_hook_response(response)
783-
if isinstance(response_, Response):
784-
response = response_
785-
else:
786-
await response.aread()
787-
788-
await response.aclose()
789-
return response
790-
791758
async def get(
792759
self,
793760
url: URLTypes,
@@ -995,6 +962,65 @@ async def delete(
995962
timeout=timeout,
996963
)
997964

965+
async def send(
966+
self,
967+
request: Request,
968+
*,
969+
stream: bool = False,
970+
auth: AuthTypes = None,
971+
follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS,
972+
) -> Response:
973+
if self._state == ClientState.CLOSED:
974+
raise RuntimeError("Cannot send a request, as the client has been closed.")
975+
976+
self._state = ClientState.OPENED
977+
for fn in [self.prepare_auth, self.build_hook_request]:
978+
request_ = fn(request, auth or self.auth, follow_redirects)
979+
if isinstance(request_, Request):
980+
request = request_
981+
982+
self.follow_redirects = follow_redirects
983+
response = await self._send(
984+
request,
985+
start=time.perf_counter(),
986+
history=[]
987+
)
988+
989+
if self.hooks.get("response"):
990+
response_ = self.build_hook_response(response)
991+
if isinstance(response_, Response):
992+
response = response_
993+
else:
994+
await response.aread()
995+
996+
await response.aclose()
997+
return response
998+
999+
async def _send(
1000+
self, request: Request, *, history: list = None, start: float = None
1001+
) -> Response:
1002+
start = start or time.perf_counter()
1003+
config = self.prepare_config(request)
1004+
response = Response.from_tls_response(
1005+
await self.session.arequest(config.to_dict()), is_byte_response=config.isByteResponse,
1006+
)
1007+
response.request = request
1008+
response.default_encoding = self.encoding
1009+
response.elapsed = datetime.timedelta(seconds=time.perf_counter() - start)
1010+
if response.is_redirect:
1011+
response.next = self._rebuild_redirect_request(response.request, response)
1012+
if self.follow_redirects:
1013+
is_break = bool(len(history) < self.max_redirects)
1014+
if not is_break:
1015+
raise TooManyRedirects("Too many redirects.")
1016+
1017+
while is_break:
1018+
history.append(response)
1019+
return await self._send(response.next, history=history, start=start)
1020+
1021+
response.history = history
1022+
return response
1023+
9981024
async def aclose(self) -> None:
9991025
return self.close()
10001026

tls_requests/models/tls.py

+14
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,24 @@ def response(cls, raw: bytes) -> "TLSResponse":
154154
cls.free_memory(response.id)
155155
return response
156156

157+
@classmethod
158+
async def aresponse(cls, raw: bytes):
159+
with StreamEncoder.from_bytes(raw) as stream:
160+
content = b"".join([chunk async for chunk in stream])
161+
return TLSResponse.from_kwargs(**to_json(content))
162+
163+
@classmethod
164+
async def arequest(cls, payload):
165+
return await cls._aread(cls._request, payload)
166+
157167
@classmethod
158168
def _send(cls, fn: callable, payload: dict):
159169
return cls.response(fn(to_bytes(payload)))
160170

171+
@classmethod
172+
async def _aread(cls, fn: callable, payload: dict):
173+
return await cls.aresponse(fn(to_bytes(payload)))
174+
161175

162176
@dataclass
163177
class _BaseConfig:

0 commit comments

Comments
 (0)