Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bugs: AsyncClient is not asynchronous #15 #16

Merged
merged 1 commit into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
include LICENSE
include README.md
include CHANGELOG.md
recursive-include tls_requests docs Makefile *.md *.rst
recursive-include tls_requests docs Makefile *.md
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ test:
rm -rf *.egg-info

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

pytest:
python -m pytest tests
Expand Down
35 changes: 29 additions & 6 deletions docs/advanced/async_client.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,37 @@ To send asynchronous HTTP requests, use the `AsyncClient`:

```pycon
>>> import asyncio
>>> async def fetch(url):
async with tls_requests.AsyncClient() as client:
r = await client.get(url)
return r
>>> import random
>>> import time
>>> import tls_requests
>>> async def fetch(idx, url):
async with tls_requests.AsyncClient() as client:
rand = random.uniform(0.1, 1.5)
start_time = time.perf_counter()
print("%s: Sleep for %.2f seconds." % (idx, rand))
await asyncio.sleep(rand)
response = await client.get(url)
end_time = time.perf_counter()
print("%s: Took: %.2f" % (idx, (end_time - start_time)))
return response
>>> async def run(urls):
tasks = [asyncio.create_task(fetch(idx, url)) for idx, url in enumerate(urls)]
responses = await asyncio.gather(*tasks)
return responses

>>> r = asyncio.run(fetch("https://httpbin.org/get"))
>>> start_urls = [
'https://httpbin.org/absolute-redirect/1',
'https://httpbin.org/absolute-redirect/2',
'https://httpbin.org/absolute-redirect/3',
'https://httpbin.org/absolute-redirect/4',
'https://httpbin.org/absolute-redirect/5',
]


>>> r = asyncio.run(run(start_urls))
>>> r
<Response [200 OK]>
[<Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>]

```

!!! tip
Expand Down
94 changes: 60 additions & 34 deletions tls_requests/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,6 @@ def _rebuild_redirect_url(self, request: Request, response: Response) -> URL:
def _send(
self, request: Request, *, history: list = None, start: float = None
) -> Response:
history = history if isinstance(history, list) else []
start = start or time.perf_counter()
config = self.prepare_config(request)
response = Response.from_tls_response(
Expand Down Expand Up @@ -483,6 +482,7 @@ def send(
response = self._send(
request,
start=time.perf_counter(),
history=[]
)

if self.hooks.get("response"):
Expand Down Expand Up @@ -755,39 +755,6 @@ async def request(
)
return await self.send(request, auth=auth, follow_redirects=follow_redirects)

async def send(
self,
request: Request,
*,
stream: bool = False,
auth: AuthTypes = None,
follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS,
) -> Response:
if self._state == ClientState.CLOSED:
raise RuntimeError("Cannot send a request, as the client has been closed.")

self._state = ClientState.OPENED
for fn in [self.prepare_auth, self.build_hook_request]:
request_ = fn(request, auth or self.auth, follow_redirects)
if isinstance(request_, Request):
request = request_

self.follow_redirects = follow_redirects
response = self._send(
request,
start=time.perf_counter(),
)

if self.hooks.get("response"):
response_ = self.build_hook_response(response)
if isinstance(response_, Response):
response = response_
else:
await response.aread()

await response.aclose()
return response

async def get(
self,
url: URLTypes,
Expand Down Expand Up @@ -995,6 +962,65 @@ async def delete(
timeout=timeout,
)

async def send(
self,
request: Request,
*,
stream: bool = False,
auth: AuthTypes = None,
follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS,
) -> Response:
if self._state == ClientState.CLOSED:
raise RuntimeError("Cannot send a request, as the client has been closed.")

self._state = ClientState.OPENED
for fn in [self.prepare_auth, self.build_hook_request]:
request_ = fn(request, auth or self.auth, follow_redirects)
if isinstance(request_, Request):
request = request_

self.follow_redirects = follow_redirects
response = await self._send(
request,
start=time.perf_counter(),
history=[]
)

if self.hooks.get("response"):
response_ = self.build_hook_response(response)
if isinstance(response_, Response):
response = response_
else:
await response.aread()

await response.aclose()
return response

async def _send(
self, request: Request, *, history: list = None, start: float = None
) -> Response:
start = start or time.perf_counter()
config = self.prepare_config(request)
response = Response.from_tls_response(
await self.session.arequest(config.to_dict()), is_byte_response=config.isByteResponse,
)
response.request = request
response.default_encoding = self.encoding
response.elapsed = datetime.timedelta(seconds=time.perf_counter() - start)
if response.is_redirect:
response.next = self._rebuild_redirect_request(response.request, response)
if self.follow_redirects:
is_break = bool(len(history) < self.max_redirects)
if not is_break:
raise TooManyRedirects("Too many redirects.")

while is_break:
history.append(response)
return await self._send(response.next, history=history, start=start)

response.history = history
return response

async def aclose(self) -> None:
return self.close()

Expand Down
14 changes: 14 additions & 0 deletions tls_requests/models/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,24 @@ def response(cls, raw: bytes) -> "TLSResponse":
cls.free_memory(response.id)
return response

@classmethod
async def aresponse(cls, raw: bytes):
with StreamEncoder.from_bytes(raw) as stream:
content = b"".join([chunk async for chunk in stream])
return TLSResponse.from_kwargs(**to_json(content))

@classmethod
async def arequest(cls, payload):
return await cls._aread(cls._request, payload)

@classmethod
def _send(cls, fn: callable, payload: dict):
return cls.response(fn(to_bytes(payload)))

@classmethod
async def _aread(cls, fn: callable, payload: dict):
return await cls.aresponse(fn(to_bytes(payload)))


@dataclass
class _BaseConfig:
Expand Down
Loading