Skip to content
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
28 changes: 14 additions & 14 deletions src/httpx2/httpx2/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,43 +132,43 @@ class ClientState(enum.Enum):

class BoundSyncStream(SyncByteStream):
"""
A byte stream that is bound to a given response instance, and that
ensures the `response.elapsed` is set once the response is closed.
A byte stream that tracks elapsed time for a response. Once closed, the
elapsed time is available via the `elapsed` attribute, and the response
can read it back from `response.stream.elapsed`.
"""

def __init__(self, stream: SyncByteStream, response: Response, start: float) -> None:
def __init__(self, stream: SyncByteStream, start: float) -> None:
self._stream = stream
self._response = response
self._start = start
self.elapsed: datetime.timedelta | None = None

def __iter__(self) -> typing.Iterator[bytes]:
for chunk in self._stream:
yield chunk

def close(self) -> None:
elapsed = time.perf_counter() - self._start
self._response.elapsed = datetime.timedelta(seconds=elapsed)
self.elapsed = datetime.timedelta(seconds=time.perf_counter() - self._start)
self._stream.close()


class BoundAsyncStream(AsyncByteStream):
"""
An async byte stream that is bound to a given response instance, and that
ensures the `response.elapsed` is set once the response is closed.
An async byte stream that tracks elapsed time for a response. Once closed,
the elapsed time is available via the `elapsed` attribute, and the response
can read it back from `response.stream.elapsed`.
"""

def __init__(self, stream: AsyncByteStream, response: Response, start: float) -> None:
def __init__(self, stream: AsyncByteStream, start: float) -> None:
self._stream = stream
self._response = response
self._start = start
self.elapsed: datetime.timedelta | None = None

async def __aiter__(self) -> typing.AsyncIterator[bytes]:
async for chunk in self._stream:
yield chunk

async def aclose(self) -> None:
elapsed = time.perf_counter() - self._start
self._response.elapsed = datetime.timedelta(seconds=elapsed)
self.elapsed = datetime.timedelta(seconds=time.perf_counter() - self._start)
await self._stream.aclose()


Expand Down Expand Up @@ -977,7 +977,7 @@ def _send_single_request(self, request: Request) -> Response:
assert isinstance(response.stream, SyncByteStream)

response.request = request
response.stream = BoundSyncStream(response.stream, response=response, start=start)
response.stream = BoundSyncStream(response.stream, start=start)
self.cookies.extract_cookies(response)
response.default_encoding = self._default_encoding

Expand Down Expand Up @@ -1680,7 +1680,7 @@ async def _send_single_request(self, request: Request) -> Response:

assert isinstance(response.stream, AsyncByteStream)
response.request = request
response.stream = BoundAsyncStream(response.stream, response=response, start=start)
response.stream = BoundAsyncStream(response.stream, start=start)
self.cookies.extract_cookies(response)
response.default_encoding = self._default_encoding

Expand Down
3 changes: 3 additions & 0 deletions src/httpx2/httpx2/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,9 @@ def elapsed(self) -> datetime.timedelta:
cycle to complete.
"""
if not hasattr(self, "_elapsed"):
stream_elapsed: datetime.timedelta | None = getattr(self.stream, "elapsed", None)
if stream_elapsed is not None:
return stream_elapsed
raise RuntimeError("'.elapsed' may only be accessed after the response has been read or closed.")
return self._elapsed

Expand Down
7 changes: 7 additions & 0 deletions tests/httpx2/models/test_responses.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import datetime
import json
import pickle
import typing
Expand Down Expand Up @@ -771,6 +772,12 @@ async def test_elapsed_not_available_until_closed() -> None:
response.elapsed # noqa: B018


def test_elapsed_set_directly() -> None:
response = httpx2.Response(200)
response.elapsed = datetime.timedelta(seconds=1)
assert response.elapsed == datetime.timedelta(seconds=1)


def test_unknown_status_code() -> None:
response = httpx2.Response(
600,
Expand Down
Loading