* Add `.elapsed` onto Response objects tracking how long a request took.
* Move elapsed timing from send methods into _get_response
* Address feedback
* Update tests/test_api.py
Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>
* `.request` - **Request**
* `.cookies` - **Cookies**
* `.history` - **List[Response]**
+* `.elapsed` - **[timedelta](https://docs.python.org/3/library/datetime.html)**
+ * The amount of time elapsed between sending the first byte and parsing the headers (not including time spent reading
+ the response). Use
+ [total_seconds()](https://docs.python.org/3/library/datetime.html#datetime.timedelta.total_seconds) to correctly get
+ the total elapsed seconds.
* `def .raise_for_status()` - **None**
* `def .json()` - **Any**
* `def .read()` - **bytes**
ResponseContent,
URLTypes,
)
-from .utils import get_netrc_login
+from .utils import ElapsedTimer, get_netrc_login
class BaseClient:
async def get_response(request: AsyncRequest) -> AsyncResponse:
try:
- response = await self.dispatch.send(
- request, verify=verify, cert=cert, timeout=timeout
- )
+ with ElapsedTimer() as timer:
+ response = await self.dispatch.send(
+ request, verify=verify, cert=cert, timeout=timeout
+ )
+ response.elapsed = timer.elapsed
except HTTPError as exc:
# Add the original request to any HTTPError
exc.request = request
on_close=sync_on_close,
request=async_response.request,
history=async_response.history,
+ elapsed=async_response.elapsed,
)
if not stream:
try:
import cgi
+import datetime
import email.message
import json as jsonlib
import typing
headers: HeaderTypes = None,
request: BaseRequest = None,
on_close: typing.Callable = None,
+ elapsed: datetime.timedelta = None,
):
self.status_code = status_code
self.http_version = http_version
self.request = request
self.on_close = on_close
+ self.elapsed = datetime.timedelta(0) if elapsed is None else elapsed
self.call_next: typing.Optional[typing.Callable] = None
@property
on_close: typing.Callable = None,
request: AsyncRequest = None,
history: typing.List["BaseResponse"] = None,
+ elapsed: datetime.timedelta = None,
):
super().__init__(
status_code=status_code,
headers=headers,
request=request,
on_close=on_close,
+ elapsed=elapsed,
)
self.history = [] if history is None else list(history)
on_close: typing.Callable = None,
request: Request = None,
history: typing.List["BaseResponse"] = None,
+ elapsed: datetime.timedelta = None,
):
super().__init__(
status_code=status_code,
headers=headers,
request=request,
on_close=on_close,
+ elapsed=elapsed,
)
self.history = [] if history is None else list(history)
import re
import sys
import typing
+from datetime import timedelta
from pathlib import Path
+from time import perf_counter
+from types import TracebackType
def normalize_header_key(value: typing.AnyStr, encoding: str = None) -> bytes:
def unquote(value: str) -> str:
return value[1:-1] if value[0] == value[-1] == '"' else value
+
+
+class ElapsedTimer:
+ def __init__(self) -> None:
+ self.start: float = perf_counter()
+ self.end: typing.Optional[float] = None
+
+ def __enter__(self) -> "ElapsedTimer":
+ self.start = perf_counter()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: typing.Type[BaseException] = None,
+ exc_value: BaseException = None,
+ traceback: TracebackType = None,
+ ) -> None:
+ self.end = perf_counter()
+
+ @property
+ def elapsed(self) -> timedelta:
+ if self.end is None:
+ return timedelta(seconds=perf_counter() - self.start)
+ return timedelta(seconds=self.end - self.start)
+from datetime import timedelta
+
import pytest
import httpx
assert response.http_version == "HTTP/1.1"
assert response.headers
assert repr(response) == "<Response [200 OK]>"
+ assert response.elapsed > timedelta(seconds=0)
async def test_build_request(server, backend):
+from datetime import timedelta
+from time import sleep
+
import pytest
import httpx
assert response.headers
assert response.is_redirect is False
assert repr(response) == "<Response [200 OK]>"
+ assert response.elapsed > timedelta(0)
def test_build_request(server):
with pytest.raises(ValueError):
httpx.Client(backend=AnyBackend())
+
+
+def test_elapsed_delay(server):
+ with httpx.Client() as http:
+ response = http.get(server.url.copy_with(path="/slow_response/100"))
+ assert response.elapsed.total_seconds() == pytest.approx(0.1, abs=0.01)
+
+
+def test_elapsed_delay_ignores_read_time(server):
+ with httpx.Client() as http:
+ response = http.get(server.url.copy_with(path="/slow_response/50"), stream=True)
+ sleep(0.1)
+ response.read()
+ assert response.elapsed.total_seconds() == pytest.approx(0.05, abs=0.01)
async def app(scope, receive, send):
assert scope["type"] == "http"
- if scope["path"] == "/slow_response":
+ if scope["path"].startswith("/slow_response"):
await slow_response(scope, receive, send)
elif scope["path"].startswith("/status"):
await status_code(scope, receive, send)
async def slow_response(scope, receive, send):
- await asyncio.sleep(0.1)
+ delay_ms_str: str = scope["path"].replace("/slow_response/", "")
+ try:
+ delay_ms = float(delay_ms_str)
+ except ValueError:
+ delay_ms = 100
+ await asyncio.sleep(delay_ms / 1000.0)
await send(
{
"type": "http.response.start",
+import datetime
import json
from unittest import mock
assert response.status_code == 200
assert response.reason_phrase == "OK"
assert response.text == "Hello, world!"
+ assert response.elapsed == datetime.timedelta(0)
def test_response_repr():
+import asyncio
import logging
import os
import httpx
from httpx import utils
-from httpx.utils import get_netrc_login, guess_json_utf, parse_header_links
+from httpx.utils import (
+ ElapsedTimer,
+ get_netrc_login,
+ guess_json_utf,
+ parse_header_links,
+)
@pytest.mark.parametrize(
# Reset the logger so we don't have verbose output in all unit tests
logging.getLogger("httpx").handlers = []
+
+
+@pytest.mark.asyncio
+async def test_elapsed_timer():
+ with ElapsedTimer() as timer:
+ assert timer.elapsed.total_seconds() == pytest.approx(0, abs=0.05)
+ await asyncio.sleep(0.1)
+ await asyncio.sleep(
+ 0.1
+ ) # test to ensure time spent after timer exits isn't accounted for.
+ assert timer.elapsed.total_seconds() == pytest.approx(0.1, abs=0.05)