Skip to content

Commit 3c28860

Browse files
feat(client): add retries_taken to raw response class (#462)
1 parent deeee53 commit 3c28860

File tree

4 files changed

+126
-1
lines changed

4 files changed

+126
-1
lines changed

src/finch/_base_client.py

+10
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,7 @@ def _request(
10501050
response=response,
10511051
stream=stream,
10521052
stream_cls=stream_cls,
1053+
retries_taken=options.get_max_retries(self.max_retries) - retries,
10531054
)
10541055

10551056
def _retry_request(
@@ -1091,6 +1092,7 @@ def _process_response(
10911092
response: httpx.Response,
10921093
stream: bool,
10931094
stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
1095+
retries_taken: int = 0,
10941096
) -> ResponseT:
10951097
if response.request.headers.get(RAW_RESPONSE_HEADER) == "true":
10961098
return cast(
@@ -1102,6 +1104,7 @@ def _process_response(
11021104
stream=stream,
11031105
stream_cls=stream_cls,
11041106
options=options,
1107+
retries_taken=retries_taken,
11051108
),
11061109
)
11071110

@@ -1121,6 +1124,7 @@ def _process_response(
11211124
stream=stream,
11221125
stream_cls=stream_cls,
11231126
options=options,
1127+
retries_taken=retries_taken,
11241128
),
11251129
)
11261130

@@ -1134,6 +1138,7 @@ def _process_response(
11341138
stream=stream,
11351139
stream_cls=stream_cls,
11361140
options=options,
1141+
retries_taken=retries_taken,
11371142
)
11381143
if bool(response.request.headers.get(RAW_RESPONSE_HEADER)):
11391144
return cast(ResponseT, api_response)
@@ -1624,6 +1629,7 @@ async def _request(
16241629
response=response,
16251630
stream=stream,
16261631
stream_cls=stream_cls,
1632+
retries_taken=options.get_max_retries(self.max_retries) - retries,
16271633
)
16281634

16291635
async def _retry_request(
@@ -1663,6 +1669,7 @@ async def _process_response(
16631669
response: httpx.Response,
16641670
stream: bool,
16651671
stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
1672+
retries_taken: int = 0,
16661673
) -> ResponseT:
16671674
if response.request.headers.get(RAW_RESPONSE_HEADER) == "true":
16681675
return cast(
@@ -1674,6 +1681,7 @@ async def _process_response(
16741681
stream=stream,
16751682
stream_cls=stream_cls,
16761683
options=options,
1684+
retries_taken=retries_taken,
16771685
),
16781686
)
16791687

@@ -1693,6 +1701,7 @@ async def _process_response(
16931701
stream=stream,
16941702
stream_cls=stream_cls,
16951703
options=options,
1704+
retries_taken=retries_taken,
16961705
),
16971706
)
16981707

@@ -1706,6 +1715,7 @@ async def _process_response(
17061715
stream=stream,
17071716
stream_cls=stream_cls,
17081717
options=options,
1718+
retries_taken=retries_taken,
17091719
)
17101720
if bool(response.request.headers.get(RAW_RESPONSE_HEADER)):
17111721
return cast(ResponseT, api_response)

src/finch/_legacy_response.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,18 @@
55
import logging
66
import datetime
77
import functools
8-
from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, Iterator, AsyncIterator, cast, overload
8+
from typing import (
9+
TYPE_CHECKING,
10+
Any,
11+
Union,
12+
Generic,
13+
TypeVar,
14+
Callable,
15+
Iterator,
16+
AsyncIterator,
17+
cast,
18+
overload,
19+
)
920
from typing_extensions import Awaitable, ParamSpec, override, deprecated, get_origin
1021

1122
import anyio
@@ -53,6 +64,9 @@ class LegacyAPIResponse(Generic[R]):
5364

5465
http_response: httpx.Response
5566

67+
retries_taken: int
68+
"""The number of retries made. If no retries happened this will be `0`"""
69+
5670
def __init__(
5771
self,
5872
*,
@@ -62,6 +76,7 @@ def __init__(
6276
stream: bool,
6377
stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
6478
options: FinalRequestOptions,
79+
retries_taken: int = 0,
6580
) -> None:
6681
self._cast_to = cast_to
6782
self._client = client
@@ -70,6 +85,7 @@ def __init__(
7085
self._stream_cls = stream_cls
7186
self._options = options
7287
self.http_response = raw
88+
self.retries_taken = retries_taken
7389

7490
@overload
7591
def parse(self, *, to: type[_T]) -> _T:

src/finch/_response.py

+5
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ class BaseAPIResponse(Generic[R]):
5555

5656
http_response: httpx.Response
5757

58+
retries_taken: int
59+
"""The number of retries made. If no retries happened this will be `0`"""
60+
5861
def __init__(
5962
self,
6063
*,
@@ -64,6 +67,7 @@ def __init__(
6467
stream: bool,
6568
stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
6669
options: FinalRequestOptions,
70+
retries_taken: int = 0,
6771
) -> None:
6872
self._cast_to = cast_to
6973
self._client = client
@@ -72,6 +76,7 @@ def __init__(
7276
self._stream_cls = stream_cls
7377
self._options = options
7478
self.http_response = raw
79+
self.retries_taken = retries_taken
7580

7681
@property
7782
def headers(self) -> httpx.Headers:

tests/test_client.py

+94
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]:
3636
return dict(url.params)
3737

3838

39+
def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float:
40+
return 0.1
41+
42+
3943
class TestFinch:
4044
client = Finch(
4145
base_url=base_url,
@@ -950,6 +954,49 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str
950954
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
951955
assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
952956

957+
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
958+
@mock.patch("finch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
959+
@pytest.mark.respx(base_url=base_url)
960+
def test_retries_taken(self, client: Finch, failures_before_success: int, respx_mock: MockRouter) -> None:
961+
client = client.with_options(max_retries=4)
962+
963+
nb_retries = 0
964+
965+
def retry_handler(_request: httpx.Request) -> httpx.Response:
966+
nonlocal nb_retries
967+
if nb_retries < failures_before_success:
968+
nb_retries += 1
969+
return httpx.Response(500)
970+
return httpx.Response(200)
971+
972+
respx_mock.get("/employer/directory").mock(side_effect=retry_handler)
973+
974+
response = client.hris.directory.with_raw_response.list()
975+
976+
assert response.retries_taken == failures_before_success
977+
978+
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
979+
@mock.patch("finch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
980+
@pytest.mark.respx(base_url=base_url)
981+
def test_retries_taken_new_response_class(
982+
self, client: Finch, failures_before_success: int, respx_mock: MockRouter
983+
) -> None:
984+
client = client.with_options(max_retries=4)
985+
986+
nb_retries = 0
987+
988+
def retry_handler(_request: httpx.Request) -> httpx.Response:
989+
nonlocal nb_retries
990+
if nb_retries < failures_before_success:
991+
nb_retries += 1
992+
return httpx.Response(500)
993+
return httpx.Response(200)
994+
995+
respx_mock.get("/employer/directory").mock(side_effect=retry_handler)
996+
997+
with client.hris.directory.with_streaming_response.list() as response:
998+
assert response.retries_taken == failures_before_success
999+
9531000

9541001
class TestAsyncFinch:
9551002
client = AsyncFinch(
@@ -1870,3 +1917,50 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte
18701917
options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
18711918
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
18721919
assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
1920+
1921+
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1922+
@mock.patch("finch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1923+
@pytest.mark.respx(base_url=base_url)
1924+
@pytest.mark.asyncio
1925+
async def test_retries_taken(
1926+
self, async_client: AsyncFinch, failures_before_success: int, respx_mock: MockRouter
1927+
) -> None:
1928+
client = async_client.with_options(max_retries=4)
1929+
1930+
nb_retries = 0
1931+
1932+
def retry_handler(_request: httpx.Request) -> httpx.Response:
1933+
nonlocal nb_retries
1934+
if nb_retries < failures_before_success:
1935+
nb_retries += 1
1936+
return httpx.Response(500)
1937+
return httpx.Response(200)
1938+
1939+
respx_mock.get("/employer/directory").mock(side_effect=retry_handler)
1940+
1941+
response = await client.hris.directory.with_raw_response.list()
1942+
1943+
assert response.retries_taken == failures_before_success
1944+
1945+
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
1946+
@mock.patch("finch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
1947+
@pytest.mark.respx(base_url=base_url)
1948+
@pytest.mark.asyncio
1949+
async def test_retries_taken_new_response_class(
1950+
self, async_client: AsyncFinch, failures_before_success: int, respx_mock: MockRouter
1951+
) -> None:
1952+
client = async_client.with_options(max_retries=4)
1953+
1954+
nb_retries = 0
1955+
1956+
def retry_handler(_request: httpx.Request) -> httpx.Response:
1957+
nonlocal nb_retries
1958+
if nb_retries < failures_before_success:
1959+
nb_retries += 1
1960+
return httpx.Response(500)
1961+
return httpx.Response(200)
1962+
1963+
respx_mock.get("/employer/directory").mock(side_effect=retry_handler)
1964+
1965+
async with client.hris.directory.with_streaming_response.list() as response:
1966+
assert response.retries_taken == failures_before_success

0 commit comments

Comments
 (0)