Skip to content

Commit a6a35ea

Browse files
stainless-app[bot]stainless-bot
authored andcommitted
feat(client): send retry count header (#500)
1 parent 9b3fa1e commit a6a35ea

File tree

2 files changed

+58
-47
lines changed

2 files changed

+58
-47
lines changed

src/finch/_base_client.py

+54-47
Original file line numberDiff line numberDiff line change
@@ -401,14 +401,7 @@ def _make_status_error(
401401
) -> _exceptions.APIStatusError:
402402
raise NotImplementedError()
403403

404-
def _remaining_retries(
405-
self,
406-
remaining_retries: Optional[int],
407-
options: FinalRequestOptions,
408-
) -> int:
409-
return remaining_retries if remaining_retries is not None else options.get_max_retries(self.max_retries)
410-
411-
def _build_headers(self, options: FinalRequestOptions) -> httpx.Headers:
404+
def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers:
412405
custom_headers = options.headers or {}
413406
headers_dict = _merge_mappings(self.default_headers, custom_headers)
414407
self._validate_headers(headers_dict, custom_headers)
@@ -420,6 +413,8 @@ def _build_headers(self, options: FinalRequestOptions) -> httpx.Headers:
420413
if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers:
421414
headers[idempotency_header] = options.idempotency_key or self._idempotency_key()
422415

416+
headers.setdefault("x-stainless-retry-count", str(retries_taken))
417+
423418
return headers
424419

425420
def _prepare_url(self, url: str) -> URL:
@@ -441,6 +436,8 @@ def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder:
441436
def _build_request(
442437
self,
443438
options: FinalRequestOptions,
439+
*,
440+
retries_taken: int = 0,
444441
) -> httpx.Request:
445442
if log.isEnabledFor(logging.DEBUG):
446443
log.debug("Request options: %s", model_dump(options, exclude_unset=True))
@@ -456,7 +453,7 @@ def _build_request(
456453
else:
457454
raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`")
458455

459-
headers = self._build_headers(options)
456+
headers = self._build_headers(options, retries_taken=retries_taken)
460457
params = _merge_mappings(self.default_query, options.params)
461458
content_type = headers.get("Content-Type")
462459
files = options.files
@@ -939,20 +936,25 @@ def request(
939936
stream: bool = False,
940937
stream_cls: type[_StreamT] | None = None,
941938
) -> ResponseT | _StreamT:
939+
if remaining_retries is not None:
940+
retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
941+
else:
942+
retries_taken = 0
943+
942944
return self._request(
943945
cast_to=cast_to,
944946
options=options,
945947
stream=stream,
946948
stream_cls=stream_cls,
947-
remaining_retries=remaining_retries,
949+
retries_taken=retries_taken,
948950
)
949951

950952
def _request(
951953
self,
952954
*,
953955
cast_to: Type[ResponseT],
954956
options: FinalRequestOptions,
955-
remaining_retries: int | None,
957+
retries_taken: int,
956958
stream: bool,
957959
stream_cls: type[_StreamT] | None,
958960
) -> ResponseT | _StreamT:
@@ -964,8 +966,8 @@ def _request(
964966
cast_to = self._maybe_override_cast_to(cast_to, options)
965967
options = self._prepare_options(options)
966968

967-
retries = self._remaining_retries(remaining_retries, options)
968-
request = self._build_request(options)
969+
remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
970+
request = self._build_request(options, retries_taken=retries_taken)
969971
self._prepare_request(request)
970972

971973
kwargs: HttpxSendArgs = {}
@@ -983,11 +985,11 @@ def _request(
983985
except httpx.TimeoutException as err:
984986
log.debug("Encountered httpx.TimeoutException", exc_info=True)
985987

986-
if retries > 0:
988+
if remaining_retries > 0:
987989
return self._retry_request(
988990
input_options,
989991
cast_to,
990-
retries,
992+
retries_taken=retries_taken,
991993
stream=stream,
992994
stream_cls=stream_cls,
993995
response_headers=None,
@@ -998,11 +1000,11 @@ def _request(
9981000
except Exception as err:
9991001
log.debug("Encountered Exception", exc_info=True)
10001002

1001-
if retries > 0:
1003+
if remaining_retries > 0:
10021004
return self._retry_request(
10031005
input_options,
10041006
cast_to,
1005-
retries,
1007+
retries_taken=retries_taken,
10061008
stream=stream,
10071009
stream_cls=stream_cls,
10081010
response_headers=None,
@@ -1025,13 +1027,13 @@ def _request(
10251027
except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
10261028
log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
10271029

1028-
if retries > 0 and self._should_retry(err.response):
1030+
if remaining_retries > 0 and self._should_retry(err.response):
10291031
err.response.close()
10301032
return self._retry_request(
10311033
input_options,
10321034
cast_to,
1033-
retries,
1034-
err.response.headers,
1035+
retries_taken=retries_taken,
1036+
response_headers=err.response.headers,
10351037
stream=stream,
10361038
stream_cls=stream_cls,
10371039
)
@@ -1050,26 +1052,26 @@ def _request(
10501052
response=response,
10511053
stream=stream,
10521054
stream_cls=stream_cls,
1053-
retries_taken=options.get_max_retries(self.max_retries) - retries,
1055+
retries_taken=retries_taken,
10541056
)
10551057

10561058
def _retry_request(
10571059
self,
10581060
options: FinalRequestOptions,
10591061
cast_to: Type[ResponseT],
1060-
remaining_retries: int,
1061-
response_headers: httpx.Headers | None,
10621062
*,
1063+
retries_taken: int,
1064+
response_headers: httpx.Headers | None,
10631065
stream: bool,
10641066
stream_cls: type[_StreamT] | None,
10651067
) -> ResponseT | _StreamT:
1066-
remaining = remaining_retries - 1
1067-
if remaining == 1:
1068+
remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
1069+
if remaining_retries == 1:
10681070
log.debug("1 retry left")
10691071
else:
1070-
log.debug("%i retries left", remaining)
1072+
log.debug("%i retries left", remaining_retries)
10711073

1072-
timeout = self._calculate_retry_timeout(remaining, options, response_headers)
1074+
timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
10731075
log.info("Retrying request to %s in %f seconds", options.url, timeout)
10741076

10751077
# In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a
@@ -1079,7 +1081,7 @@ def _retry_request(
10791081
return self._request(
10801082
options=options,
10811083
cast_to=cast_to,
1082-
remaining_retries=remaining,
1084+
retries_taken=retries_taken + 1,
10831085
stream=stream,
10841086
stream_cls=stream_cls,
10851087
)
@@ -1511,12 +1513,17 @@ async def request(
15111513
stream_cls: type[_AsyncStreamT] | None = None,
15121514
remaining_retries: Optional[int] = None,
15131515
) -> ResponseT | _AsyncStreamT:
1516+
if remaining_retries is not None:
1517+
retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
1518+
else:
1519+
retries_taken = 0
1520+
15141521
return await self._request(
15151522
cast_to=cast_to,
15161523
options=options,
15171524
stream=stream,
15181525
stream_cls=stream_cls,
1519-
remaining_retries=remaining_retries,
1526+
retries_taken=retries_taken,
15201527
)
15211528

15221529
async def _request(
@@ -1526,7 +1533,7 @@ async def _request(
15261533
*,
15271534
stream: bool,
15281535
stream_cls: type[_AsyncStreamT] | None,
1529-
remaining_retries: int | None,
1536+
retries_taken: int,
15301537
) -> ResponseT | _AsyncStreamT:
15311538
if self._platform is None:
15321539
# `get_platform` can make blocking IO calls so we
@@ -1541,8 +1548,8 @@ async def _request(
15411548
cast_to = self._maybe_override_cast_to(cast_to, options)
15421549
options = await self._prepare_options(options)
15431550

1544-
retries = self._remaining_retries(remaining_retries, options)
1545-
request = self._build_request(options)
1551+
remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
1552+
request = self._build_request(options, retries_taken=retries_taken)
15461553
await self._prepare_request(request)
15471554

15481555
kwargs: HttpxSendArgs = {}
@@ -1558,11 +1565,11 @@ async def _request(
15581565
except httpx.TimeoutException as err:
15591566
log.debug("Encountered httpx.TimeoutException", exc_info=True)
15601567

1561-
if retries > 0:
1568+
if remaining_retries > 0:
15621569
return await self._retry_request(
15631570
input_options,
15641571
cast_to,
1565-
retries,
1572+
retries_taken=retries_taken,
15661573
stream=stream,
15671574
stream_cls=stream_cls,
15681575
response_headers=None,
@@ -1573,11 +1580,11 @@ async def _request(
15731580
except Exception as err:
15741581
log.debug("Encountered Exception", exc_info=True)
15751582

1576-
if retries > 0:
1583+
if retries_taken > 0:
15771584
return await self._retry_request(
15781585
input_options,
15791586
cast_to,
1580-
retries,
1587+
retries_taken=retries_taken,
15811588
stream=stream,
15821589
stream_cls=stream_cls,
15831590
response_headers=None,
@@ -1595,13 +1602,13 @@ async def _request(
15951602
except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
15961603
log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
15971604

1598-
if retries > 0 and self._should_retry(err.response):
1605+
if remaining_retries > 0 and self._should_retry(err.response):
15991606
await err.response.aclose()
16001607
return await self._retry_request(
16011608
input_options,
16021609
cast_to,
1603-
retries,
1604-
err.response.headers,
1610+
retries_taken=retries_taken,
1611+
response_headers=err.response.headers,
16051612
stream=stream,
16061613
stream_cls=stream_cls,
16071614
)
@@ -1620,34 +1627,34 @@ async def _request(
16201627
response=response,
16211628
stream=stream,
16221629
stream_cls=stream_cls,
1623-
retries_taken=options.get_max_retries(self.max_retries) - retries,
1630+
retries_taken=retries_taken,
16241631
)
16251632

16261633
async def _retry_request(
16271634
self,
16281635
options: FinalRequestOptions,
16291636
cast_to: Type[ResponseT],
1630-
remaining_retries: int,
1631-
response_headers: httpx.Headers | None,
16321637
*,
1638+
retries_taken: int,
1639+
response_headers: httpx.Headers | None,
16331640
stream: bool,
16341641
stream_cls: type[_AsyncStreamT] | None,
16351642
) -> ResponseT | _AsyncStreamT:
1636-
remaining = remaining_retries - 1
1637-
if remaining == 1:
1643+
remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
1644+
if remaining_retries == 1:
16381645
log.debug("1 retry left")
16391646
else:
1640-
log.debug("%i retries left", remaining)
1647+
log.debug("%i retries left", remaining_retries)
16411648

1642-
timeout = self._calculate_retry_timeout(remaining, options, response_headers)
1649+
timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
16431650
log.info("Retrying request to %s in %f seconds", options.url, timeout)
16441651

16451652
await anyio.sleep(timeout)
16461653

16471654
return await self._request(
16481655
options=options,
16491656
cast_to=cast_to,
1650-
remaining_retries=remaining,
1657+
retries_taken=retries_taken + 1,
16511658
stream=stream,
16521659
stream_cls=stream_cls,
16531660
)

tests/test_client.py

+4
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
974974
response = client.hris.directory.with_raw_response.list()
975975

976976
assert response.retries_taken == failures_before_success
977+
assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
977978

978979
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
979980
@mock.patch("finch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@@ -996,6 +997,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
996997

997998
with client.hris.directory.with_streaming_response.list() as response:
998999
assert response.retries_taken == failures_before_success
1000+
assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
9991001

10001002

10011003
class TestAsyncFinch:
@@ -1941,6 +1943,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
19411943
response = await client.hris.directory.with_raw_response.list()
19421944

19431945
assert response.retries_taken == failures_before_success
1946+
assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
19441947

19451948
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
19461949
@mock.patch("finch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@@ -1964,3 +1967,4 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
19641967

19651968
async with client.hris.directory.with_streaming_response.list() as response:
19661969
assert response.retries_taken == failures_before_success
1970+
assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success

0 commit comments

Comments
 (0)