isaacus 0.4.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. isaacus-0.6.0/.release-please-manifest.json +3 -0
  2. {isaacus-0.4.0 → isaacus-0.6.0}/CHANGELOG.md +36 -0
  3. {isaacus-0.4.0 → isaacus-0.6.0}/PKG-INFO +1 -1
  4. {isaacus-0.4.0 → isaacus-0.6.0}/api.md +14 -0
  5. {isaacus-0.4.0 → isaacus-0.6.0}/pyproject.toml +1 -1
  6. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_base_client.py +175 -239
  7. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_client.py +10 -4
  8. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_models.py +2 -2
  9. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_response.py +1 -1
  10. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_utils/_utils.py +9 -1
  11. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_version.py +1 -1
  12. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/resources/__init__.py +14 -0
  13. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/resources/classifications/universal.py +3 -6
  14. isaacus-0.6.0/src/isaacus/resources/extractions/__init__.py +33 -0
  15. isaacus-0.6.0/src/isaacus/resources/extractions/extractions.py +102 -0
  16. isaacus-0.6.0/src/isaacus/resources/extractions/qa.py +258 -0
  17. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/resources/rerankings.py +3 -6
  18. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/types/classifications/universal_classification.py +3 -2
  19. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/types/classifications/universal_create_params.py +1 -1
  20. isaacus-0.6.0/src/isaacus/types/extractions/__init__.py +6 -0
  21. isaacus-0.6.0/src/isaacus/types/extractions/answer_extraction.py +71 -0
  22. isaacus-0.6.0/src/isaacus/types/extractions/qa_create_params.py +64 -0
  23. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/types/reranking_create_params.py +1 -1
  24. isaacus-0.6.0/tests/api_resources/extractions/__init__.py +1 -0
  25. isaacus-0.6.0/tests/api_resources/extractions/test_qa.py +152 -0
  26. isaacus-0.4.0/.release-please-manifest.json +0 -3
  27. {isaacus-0.4.0 → isaacus-0.6.0}/.gitignore +0 -0
  28. {isaacus-0.4.0 → isaacus-0.6.0}/CONTRIBUTING.md +0 -0
  29. {isaacus-0.4.0 → isaacus-0.6.0}/LICENSE +0 -0
  30. {isaacus-0.4.0 → isaacus-0.6.0}/README.md +0 -0
  31. {isaacus-0.4.0 → isaacus-0.6.0}/SECURITY.md +0 -0
  32. {isaacus-0.4.0 → isaacus-0.6.0}/bin/check-release-environment +0 -0
  33. {isaacus-0.4.0 → isaacus-0.6.0}/bin/publish-pypi +0 -0
  34. {isaacus-0.4.0 → isaacus-0.6.0}/examples/.keep +0 -0
  35. {isaacus-0.4.0 → isaacus-0.6.0}/mypy.ini +0 -0
  36. {isaacus-0.4.0 → isaacus-0.6.0}/noxfile.py +0 -0
  37. {isaacus-0.4.0 → isaacus-0.6.0}/release-please-config.json +0 -0
  38. {isaacus-0.4.0 → isaacus-0.6.0}/requirements-dev.lock +0 -0
  39. {isaacus-0.4.0 → isaacus-0.6.0}/requirements.lock +0 -0
  40. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/__init__.py +0 -0
  41. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_compat.py +0 -0
  42. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_constants.py +0 -0
  43. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_exceptions.py +0 -0
  44. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_files.py +0 -0
  45. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_qs.py +0 -0
  46. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_resource.py +0 -0
  47. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_streaming.py +0 -0
  48. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_types.py +0 -0
  49. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_utils/__init__.py +0 -0
  50. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_utils/_logs.py +0 -0
  51. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_utils/_proxy.py +0 -0
  52. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_utils/_reflection.py +0 -0
  53. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_utils/_streams.py +0 -0
  54. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_utils/_sync.py +0 -0
  55. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_utils/_transform.py +0 -0
  56. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/_utils/_typing.py +0 -0
  57. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/lib/.keep +0 -0
  58. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/py.typed +0 -0
  59. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/resources/classifications/__init__.py +0 -0
  60. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/resources/classifications/classifications.py +0 -0
  61. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/types/__init__.py +0 -0
  62. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/types/classifications/__init__.py +0 -0
  63. {isaacus-0.4.0 → isaacus-0.6.0}/src/isaacus/types/reranking.py +0 -0
  64. {isaacus-0.4.0 → isaacus-0.6.0}/tests/__init__.py +0 -0
  65. {isaacus-0.4.0 → isaacus-0.6.0}/tests/api_resources/__init__.py +0 -0
  66. {isaacus-0.4.0 → isaacus-0.6.0}/tests/api_resources/classifications/__init__.py +0 -0
  67. {isaacus-0.4.0 → isaacus-0.6.0}/tests/api_resources/classifications/test_universal.py +0 -0
  68. {isaacus-0.4.0 → isaacus-0.6.0}/tests/api_resources/test_rerankings.py +0 -0
  69. {isaacus-0.4.0 → isaacus-0.6.0}/tests/conftest.py +0 -0
  70. {isaacus-0.4.0 → isaacus-0.6.0}/tests/sample_file.txt +0 -0
  71. {isaacus-0.4.0 → isaacus-0.6.0}/tests/test_client.py +0 -0
  72. {isaacus-0.4.0 → isaacus-0.6.0}/tests/test_deepcopy.py +0 -0
  73. {isaacus-0.4.0 → isaacus-0.6.0}/tests/test_extract_files.py +0 -0
  74. {isaacus-0.4.0 → isaacus-0.6.0}/tests/test_files.py +0 -0
  75. {isaacus-0.4.0 → isaacus-0.6.0}/tests/test_models.py +0 -0
  76. {isaacus-0.4.0 → isaacus-0.6.0}/tests/test_qs.py +0 -0
  77. {isaacus-0.4.0 → isaacus-0.6.0}/tests/test_required_args.py +0 -0
  78. {isaacus-0.4.0 → isaacus-0.6.0}/tests/test_response.py +0 -0
  79. {isaacus-0.4.0 → isaacus-0.6.0}/tests/test_streaming.py +0 -0
  80. {isaacus-0.4.0 → isaacus-0.6.0}/tests/test_transform.py +0 -0
  81. {isaacus-0.4.0 → isaacus-0.6.0}/tests/test_utils/test_proxy.py +0 -0
  82. {isaacus-0.4.0 → isaacus-0.6.0}/tests/test_utils/test_typing.py +0 -0
  83. {isaacus-0.4.0 → isaacus-0.6.0}/tests/utils.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.6.0"
3
+ }
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 (2025-04-30)
4
+
5
+ Full Changelog: [v0.5.0...v0.6.0](https://github.com/isaacus-dev/isaacus-python/compare/v0.5.0...v0.6.0)
6
+
7
+ ### Features
8
+
9
+ * **api:** introduced extractive QA ([7b9856c](https://github.com/isaacus-dev/isaacus-python/commit/7b9856c7a64fd4694d0fe8436934fa520faa38cc))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **pydantic v1:** more robust ModelField.annotation check ([40be0d5](https://github.com/isaacus-dev/isaacus-python/commit/40be0d5d7bb0c4d5187c0207e6470800e9827216))
15
+
16
+
17
+ ### Chores
18
+
19
+ * broadly detect json family of content-type headers ([ef18419](https://github.com/isaacus-dev/isaacus-python/commit/ef18419dc26bba05aec8f5e29711bcc6fe329e9e))
20
+ * **ci:** add timeout thresholds for CI jobs ([f0438ce](https://github.com/isaacus-dev/isaacus-python/commit/f0438cebcfc587af81d967e610dc33ea5a53bb32))
21
+ * **ci:** only use depot for staging repos ([869c0ff](https://github.com/isaacus-dev/isaacus-python/commit/869c0ff5824ccfd63a4123a026530df11352db44))
22
+ * **internal:** codegen related update ([8860ae0](https://github.com/isaacus-dev/isaacus-python/commit/8860ae0393429d660038ce1c8d15020a42141979))
23
+ * **internal:** fix list file params ([6dc4e32](https://github.com/isaacus-dev/isaacus-python/commit/6dc4e32ab00e83d2307bfb729222f66f24a1f45f))
24
+ * **internal:** import reformatting ([57473e2](https://github.com/isaacus-dev/isaacus-python/commit/57473e25e03b551ab85b4d2ec484defdcc2de09d))
25
+ * **internal:** refactor retries to not use recursion ([513599c](https://github.com/isaacus-dev/isaacus-python/commit/513599ce261e2ec9a034715e20ec150025186255))
26
+
27
+ ## 0.5.0 (2025-04-19)
28
+
29
+ Full Changelog: [v0.4.0...v0.5.0](https://github.com/isaacus-dev/isaacus-python/compare/v0.4.0...v0.5.0)
30
+
31
+ ### ⚠ BREAKING CHANGES
32
+
33
+ * **api:** changed how end offsets are computed
34
+
35
+ ### Features
36
+
37
+ * **api:** changed how end offsets are computed ([3c96279](https://github.com/isaacus-dev/isaacus-python/commit/3c962792d88ec5abd6ee71d9388cc1a1ba6a80dd))
38
+
3
39
  ## 0.4.0 (2025-04-19)
4
40
 
5
41
  Full Changelog: [v0.3.3...v0.4.0](https://github.com/isaacus-dev/isaacus-python/compare/v0.3.3...v0.4.0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: isaacus
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: The official Python library for the isaacus API
5
5
  Project-URL: Homepage, https://github.com/isaacus-dev/isaacus-python
6
6
  Project-URL: Repository, https://github.com/isaacus-dev/isaacus-python
@@ -23,3 +23,17 @@ from isaacus.types import Reranking
23
23
  Methods:
24
24
 
25
25
  - <code title="post /rerankings">client.rerankings.<a href="./src/isaacus/resources/rerankings.py">create</a>(\*\*<a href="src/isaacus/types/reranking_create_params.py">params</a>) -> <a href="./src/isaacus/types/reranking.py">Reranking</a></code>
26
+
27
+ # Extractions
28
+
29
+ ## Qa
30
+
31
+ Types:
32
+
33
+ ```python
34
+ from isaacus.types.extractions import AnswerExtraction
35
+ ```
36
+
37
+ Methods:
38
+
39
+ - <code title="post /extractions/qa">client.extractions.qa.<a href="./src/isaacus/resources/extractions/qa.py">create</a>(\*\*<a href="src/isaacus/types/extractions/qa_create_params.py">params</a>) -> <a href="./src/isaacus/types/extractions/answer_extraction.py">AnswerExtraction</a></code>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "isaacus"
3
- version = "0.4.0"
3
+ version = "0.6.0"
4
4
  description = "The official Python library for the isaacus API"
5
5
  dynamic = ["readme"]
6
6
  license = "Apache-2.0"
@@ -412,8 +412,7 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
412
412
  headers = httpx.Headers(headers_dict)
413
413
 
414
414
  idempotency_header = self._idempotency_header
415
- if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers:
416
- options.idempotency_key = options.idempotency_key or self._idempotency_key()
415
+ if idempotency_header and options.idempotency_key and idempotency_header not in headers:
417
416
  headers[idempotency_header] = options.idempotency_key
418
417
 
419
418
  # Don't set these headers if they were already set or removed by the caller. We check
@@ -878,7 +877,6 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
878
877
  self,
879
878
  cast_to: Type[ResponseT],
880
879
  options: FinalRequestOptions,
881
- remaining_retries: Optional[int] = None,
882
880
  *,
883
881
  stream: Literal[True],
884
882
  stream_cls: Type[_StreamT],
@@ -889,7 +887,6 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
889
887
  self,
890
888
  cast_to: Type[ResponseT],
891
889
  options: FinalRequestOptions,
892
- remaining_retries: Optional[int] = None,
893
890
  *,
894
891
  stream: Literal[False] = False,
895
892
  ) -> ResponseT: ...
@@ -899,7 +896,6 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
899
896
  self,
900
897
  cast_to: Type[ResponseT],
901
898
  options: FinalRequestOptions,
902
- remaining_retries: Optional[int] = None,
903
899
  *,
904
900
  stream: bool = False,
905
901
  stream_cls: Type[_StreamT] | None = None,
@@ -909,125 +905,109 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
909
905
  self,
910
906
  cast_to: Type[ResponseT],
911
907
  options: FinalRequestOptions,
912
- remaining_retries: Optional[int] = None,
913
908
  *,
914
909
  stream: bool = False,
915
910
  stream_cls: type[_StreamT] | None = None,
916
911
  ) -> ResponseT | _StreamT:
917
- if remaining_retries is not None:
918
- retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
919
- else:
920
- retries_taken = 0
921
-
922
- return self._request(
923
- cast_to=cast_to,
924
- options=options,
925
- stream=stream,
926
- stream_cls=stream_cls,
927
- retries_taken=retries_taken,
928
- )
912
+ cast_to = self._maybe_override_cast_to(cast_to, options)
929
913
 
930
- def _request(
931
- self,
932
- *,
933
- cast_to: Type[ResponseT],
934
- options: FinalRequestOptions,
935
- retries_taken: int,
936
- stream: bool,
937
- stream_cls: type[_StreamT] | None,
938
- ) -> ResponseT | _StreamT:
939
914
  # create a copy of the options we were given so that if the
940
915
  # options are mutated later & we then retry, the retries are
941
916
  # given the original options
942
917
  input_options = model_copy(options)
943
-
944
- cast_to = self._maybe_override_cast_to(cast_to, options)
945
- options = self._prepare_options(options)
946
-
947
- remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
948
- request = self._build_request(options, retries_taken=retries_taken)
949
- self._prepare_request(request)
950
-
951
- if options.idempotency_key:
918
+ if input_options.idempotency_key is None and input_options.method.lower() != "get":
952
919
  # ensure the idempotency key is reused between requests
953
- input_options.idempotency_key = options.idempotency_key
920
+ input_options.idempotency_key = self._idempotency_key()
954
921
 
955
- kwargs: HttpxSendArgs = {}
956
- if self.custom_auth is not None:
957
- kwargs["auth"] = self.custom_auth
922
+ response: httpx.Response | None = None
923
+ max_retries = input_options.get_max_retries(self.max_retries)
958
924
 
959
- log.debug("Sending HTTP Request: %s %s", request.method, request.url)
925
+ retries_taken = 0
926
+ for retries_taken in range(max_retries + 1):
927
+ options = model_copy(input_options)
928
+ options = self._prepare_options(options)
960
929
 
961
- try:
962
- response = self._client.send(
963
- request,
964
- stream=stream or self._should_stream_response_body(request=request),
965
- **kwargs,
966
- )
967
- except httpx.TimeoutException as err:
968
- log.debug("Encountered httpx.TimeoutException", exc_info=True)
930
+ remaining_retries = max_retries - retries_taken
931
+ request = self._build_request(options, retries_taken=retries_taken)
932
+ self._prepare_request(request)
969
933
 
970
- if remaining_retries > 0:
971
- return self._retry_request(
972
- input_options,
973
- cast_to,
974
- retries_taken=retries_taken,
975
- stream=stream,
976
- stream_cls=stream_cls,
977
- response_headers=None,
978
- )
934
+ kwargs: HttpxSendArgs = {}
935
+ if self.custom_auth is not None:
936
+ kwargs["auth"] = self.custom_auth
979
937
 
980
- log.debug("Raising timeout error")
981
- raise APITimeoutError(request=request) from err
982
- except Exception as err:
983
- log.debug("Encountered Exception", exc_info=True)
938
+ log.debug("Sending HTTP Request: %s %s", request.method, request.url)
984
939
 
985
- if remaining_retries > 0:
986
- return self._retry_request(
987
- input_options,
988
- cast_to,
989
- retries_taken=retries_taken,
990
- stream=stream,
991
- stream_cls=stream_cls,
992
- response_headers=None,
940
+ response = None
941
+ try:
942
+ response = self._client.send(
943
+ request,
944
+ stream=stream or self._should_stream_response_body(request=request),
945
+ **kwargs,
993
946
  )
947
+ except httpx.TimeoutException as err:
948
+ log.debug("Encountered httpx.TimeoutException", exc_info=True)
949
+
950
+ if remaining_retries > 0:
951
+ self._sleep_for_retry(
952
+ retries_taken=retries_taken,
953
+ max_retries=max_retries,
954
+ options=input_options,
955
+ response=None,
956
+ )
957
+ continue
958
+
959
+ log.debug("Raising timeout error")
960
+ raise APITimeoutError(request=request) from err
961
+ except Exception as err:
962
+ log.debug("Encountered Exception", exc_info=True)
963
+
964
+ if remaining_retries > 0:
965
+ self._sleep_for_retry(
966
+ retries_taken=retries_taken,
967
+ max_retries=max_retries,
968
+ options=input_options,
969
+ response=None,
970
+ )
971
+ continue
972
+
973
+ log.debug("Raising connection error")
974
+ raise APIConnectionError(request=request) from err
975
+
976
+ log.debug(
977
+ 'HTTP Response: %s %s "%i %s" %s',
978
+ request.method,
979
+ request.url,
980
+ response.status_code,
981
+ response.reason_phrase,
982
+ response.headers,
983
+ )
994
984
 
995
- log.debug("Raising connection error")
996
- raise APIConnectionError(request=request) from err
997
-
998
- log.debug(
999
- 'HTTP Response: %s %s "%i %s" %s',
1000
- request.method,
1001
- request.url,
1002
- response.status_code,
1003
- response.reason_phrase,
1004
- response.headers,
1005
- )
985
+ try:
986
+ response.raise_for_status()
987
+ except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
988
+ log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
989
+
990
+ if remaining_retries > 0 and self._should_retry(err.response):
991
+ err.response.close()
992
+ self._sleep_for_retry(
993
+ retries_taken=retries_taken,
994
+ max_retries=max_retries,
995
+ options=input_options,
996
+ response=response,
997
+ )
998
+ continue
1006
999
 
1007
- try:
1008
- response.raise_for_status()
1009
- except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
1010
- log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
1011
-
1012
- if remaining_retries > 0 and self._should_retry(err.response):
1013
- err.response.close()
1014
- return self._retry_request(
1015
- input_options,
1016
- cast_to,
1017
- retries_taken=retries_taken,
1018
- response_headers=err.response.headers,
1019
- stream=stream,
1020
- stream_cls=stream_cls,
1021
- )
1000
+ # If the response is streamed then we need to explicitly read the response
1001
+ # to completion before attempting to access the response text.
1002
+ if not err.response.is_closed:
1003
+ err.response.read()
1022
1004
 
1023
- # If the response is streamed then we need to explicitly read the response
1024
- # to completion before attempting to access the response text.
1025
- if not err.response.is_closed:
1026
- err.response.read()
1005
+ log.debug("Re-raising status error")
1006
+ raise self._make_status_error_from_response(err.response) from None
1027
1007
 
1028
- log.debug("Re-raising status error")
1029
- raise self._make_status_error_from_response(err.response) from None
1008
+ break
1030
1009
 
1010
+ assert response is not None, "could not resolve response (should never happen)"
1031
1011
  return self._process_response(
1032
1012
  cast_to=cast_to,
1033
1013
  options=options,
@@ -1037,37 +1017,20 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
1037
1017
  retries_taken=retries_taken,
1038
1018
  )
1039
1019
 
1040
- def _retry_request(
1041
- self,
1042
- options: FinalRequestOptions,
1043
- cast_to: Type[ResponseT],
1044
- *,
1045
- retries_taken: int,
1046
- response_headers: httpx.Headers | None,
1047
- stream: bool,
1048
- stream_cls: type[_StreamT] | None,
1049
- ) -> ResponseT | _StreamT:
1050
- remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
1020
+ def _sleep_for_retry(
1021
+ self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None
1022
+ ) -> None:
1023
+ remaining_retries = max_retries - retries_taken
1051
1024
  if remaining_retries == 1:
1052
1025
  log.debug("1 retry left")
1053
1026
  else:
1054
1027
  log.debug("%i retries left", remaining_retries)
1055
1028
 
1056
- timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
1029
+ timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None)
1057
1030
  log.info("Retrying request to %s in %f seconds", options.url, timeout)
1058
1031
 
1059
- # In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a
1060
- # different thread if necessary.
1061
1032
  time.sleep(timeout)
1062
1033
 
1063
- return self._request(
1064
- options=options,
1065
- cast_to=cast_to,
1066
- retries_taken=retries_taken + 1,
1067
- stream=stream,
1068
- stream_cls=stream_cls,
1069
- )
1070
-
1071
1034
  def _process_response(
1072
1035
  self,
1073
1036
  *,
@@ -1411,7 +1374,6 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1411
1374
  options: FinalRequestOptions,
1412
1375
  *,
1413
1376
  stream: Literal[False] = False,
1414
- remaining_retries: Optional[int] = None,
1415
1377
  ) -> ResponseT: ...
1416
1378
 
1417
1379
  @overload
@@ -1422,7 +1384,6 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1422
1384
  *,
1423
1385
  stream: Literal[True],
1424
1386
  stream_cls: type[_AsyncStreamT],
1425
- remaining_retries: Optional[int] = None,
1426
1387
  ) -> _AsyncStreamT: ...
1427
1388
 
1428
1389
  @overload
@@ -1433,7 +1394,6 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1433
1394
  *,
1434
1395
  stream: bool,
1435
1396
  stream_cls: type[_AsyncStreamT] | None = None,
1436
- remaining_retries: Optional[int] = None,
1437
1397
  ) -> ResponseT | _AsyncStreamT: ...
1438
1398
 
1439
1399
  async def request(
@@ -1443,120 +1403,111 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1443
1403
  *,
1444
1404
  stream: bool = False,
1445
1405
  stream_cls: type[_AsyncStreamT] | None = None,
1446
- remaining_retries: Optional[int] = None,
1447
- ) -> ResponseT | _AsyncStreamT:
1448
- if remaining_retries is not None:
1449
- retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
1450
- else:
1451
- retries_taken = 0
1452
-
1453
- return await self._request(
1454
- cast_to=cast_to,
1455
- options=options,
1456
- stream=stream,
1457
- stream_cls=stream_cls,
1458
- retries_taken=retries_taken,
1459
- )
1460
-
1461
- async def _request(
1462
- self,
1463
- cast_to: Type[ResponseT],
1464
- options: FinalRequestOptions,
1465
- *,
1466
- stream: bool,
1467
- stream_cls: type[_AsyncStreamT] | None,
1468
- retries_taken: int,
1469
1406
  ) -> ResponseT | _AsyncStreamT:
1470
1407
  if self._platform is None:
1471
1408
  # `get_platform` can make blocking IO calls so we
1472
1409
  # execute it earlier while we are in an async context
1473
1410
  self._platform = await asyncify(get_platform)()
1474
1411
 
1412
+ cast_to = self._maybe_override_cast_to(cast_to, options)
1413
+
1475
1414
  # create a copy of the options we were given so that if the
1476
1415
  # options are mutated later & we then retry, the retries are
1477
1416
  # given the original options
1478
1417
  input_options = model_copy(options)
1479
-
1480
- cast_to = self._maybe_override_cast_to(cast_to, options)
1481
- options = await self._prepare_options(options)
1482
-
1483
- remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
1484
- request = self._build_request(options, retries_taken=retries_taken)
1485
- await self._prepare_request(request)
1486
-
1487
- if options.idempotency_key:
1418
+ if input_options.idempotency_key is None and input_options.method.lower() != "get":
1488
1419
  # ensure the idempotency key is reused between requests
1489
- input_options.idempotency_key = options.idempotency_key
1420
+ input_options.idempotency_key = self._idempotency_key()
1490
1421
 
1491
- kwargs: HttpxSendArgs = {}
1492
- if self.custom_auth is not None:
1493
- kwargs["auth"] = self.custom_auth
1422
+ response: httpx.Response | None = None
1423
+ max_retries = input_options.get_max_retries(self.max_retries)
1494
1424
 
1495
- try:
1496
- response = await self._client.send(
1497
- request,
1498
- stream=stream or self._should_stream_response_body(request=request),
1499
- **kwargs,
1500
- )
1501
- except httpx.TimeoutException as err:
1502
- log.debug("Encountered httpx.TimeoutException", exc_info=True)
1425
+ retries_taken = 0
1426
+ for retries_taken in range(max_retries + 1):
1427
+ options = model_copy(input_options)
1428
+ options = await self._prepare_options(options)
1503
1429
 
1504
- if remaining_retries > 0:
1505
- return await self._retry_request(
1506
- input_options,
1507
- cast_to,
1508
- retries_taken=retries_taken,
1509
- stream=stream,
1510
- stream_cls=stream_cls,
1511
- response_headers=None,
1512
- )
1430
+ remaining_retries = max_retries - retries_taken
1431
+ request = self._build_request(options, retries_taken=retries_taken)
1432
+ await self._prepare_request(request)
1513
1433
 
1514
- log.debug("Raising timeout error")
1515
- raise APITimeoutError(request=request) from err
1516
- except Exception as err:
1517
- log.debug("Encountered Exception", exc_info=True)
1434
+ kwargs: HttpxSendArgs = {}
1435
+ if self.custom_auth is not None:
1436
+ kwargs["auth"] = self.custom_auth
1518
1437
 
1519
- if remaining_retries > 0:
1520
- return await self._retry_request(
1521
- input_options,
1522
- cast_to,
1523
- retries_taken=retries_taken,
1524
- stream=stream,
1525
- stream_cls=stream_cls,
1526
- response_headers=None,
1527
- )
1438
+ log.debug("Sending HTTP Request: %s %s", request.method, request.url)
1528
1439
 
1529
- log.debug("Raising connection error")
1530
- raise APIConnectionError(request=request) from err
1440
+ response = None
1441
+ try:
1442
+ response = await self._client.send(
1443
+ request,
1444
+ stream=stream or self._should_stream_response_body(request=request),
1445
+ **kwargs,
1446
+ )
1447
+ except httpx.TimeoutException as err:
1448
+ log.debug("Encountered httpx.TimeoutException", exc_info=True)
1449
+
1450
+ if remaining_retries > 0:
1451
+ await self._sleep_for_retry(
1452
+ retries_taken=retries_taken,
1453
+ max_retries=max_retries,
1454
+ options=input_options,
1455
+ response=None,
1456
+ )
1457
+ continue
1458
+
1459
+ log.debug("Raising timeout error")
1460
+ raise APITimeoutError(request=request) from err
1461
+ except Exception as err:
1462
+ log.debug("Encountered Exception", exc_info=True)
1463
+
1464
+ if remaining_retries > 0:
1465
+ await self._sleep_for_retry(
1466
+ retries_taken=retries_taken,
1467
+ max_retries=max_retries,
1468
+ options=input_options,
1469
+ response=None,
1470
+ )
1471
+ continue
1472
+
1473
+ log.debug("Raising connection error")
1474
+ raise APIConnectionError(request=request) from err
1475
+
1476
+ log.debug(
1477
+ 'HTTP Response: %s %s "%i %s" %s',
1478
+ request.method,
1479
+ request.url,
1480
+ response.status_code,
1481
+ response.reason_phrase,
1482
+ response.headers,
1483
+ )
1531
1484
 
1532
- log.debug(
1533
- 'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase
1534
- )
1485
+ try:
1486
+ response.raise_for_status()
1487
+ except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
1488
+ log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
1489
+
1490
+ if remaining_retries > 0 and self._should_retry(err.response):
1491
+ await err.response.aclose()
1492
+ await self._sleep_for_retry(
1493
+ retries_taken=retries_taken,
1494
+ max_retries=max_retries,
1495
+ options=input_options,
1496
+ response=response,
1497
+ )
1498
+ continue
1535
1499
 
1536
- try:
1537
- response.raise_for_status()
1538
- except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
1539
- log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
1540
-
1541
- if remaining_retries > 0 and self._should_retry(err.response):
1542
- await err.response.aclose()
1543
- return await self._retry_request(
1544
- input_options,
1545
- cast_to,
1546
- retries_taken=retries_taken,
1547
- response_headers=err.response.headers,
1548
- stream=stream,
1549
- stream_cls=stream_cls,
1550
- )
1500
+ # If the response is streamed then we need to explicitly read the response
1501
+ # to completion before attempting to access the response text.
1502
+ if not err.response.is_closed:
1503
+ await err.response.aread()
1551
1504
 
1552
- # If the response is streamed then we need to explicitly read the response
1553
- # to completion before attempting to access the response text.
1554
- if not err.response.is_closed:
1555
- await err.response.aread()
1505
+ log.debug("Re-raising status error")
1506
+ raise self._make_status_error_from_response(err.response) from None
1556
1507
 
1557
- log.debug("Re-raising status error")
1558
- raise self._make_status_error_from_response(err.response) from None
1508
+ break
1559
1509
 
1510
+ assert response is not None, "could not resolve response (should never happen)"
1560
1511
  return await self._process_response(
1561
1512
  cast_to=cast_to,
1562
1513
  options=options,
@@ -1566,35 +1517,20 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1566
1517
  retries_taken=retries_taken,
1567
1518
  )
1568
1519
 
1569
- async def _retry_request(
1570
- self,
1571
- options: FinalRequestOptions,
1572
- cast_to: Type[ResponseT],
1573
- *,
1574
- retries_taken: int,
1575
- response_headers: httpx.Headers | None,
1576
- stream: bool,
1577
- stream_cls: type[_AsyncStreamT] | None,
1578
- ) -> ResponseT | _AsyncStreamT:
1579
- remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
1520
+ async def _sleep_for_retry(
1521
+ self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None
1522
+ ) -> None:
1523
+ remaining_retries = max_retries - retries_taken
1580
1524
  if remaining_retries == 1:
1581
1525
  log.debug("1 retry left")
1582
1526
  else:
1583
1527
  log.debug("%i retries left", remaining_retries)
1584
1528
 
1585
- timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
1529
+ timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None)
1586
1530
  log.info("Retrying request to %s in %f seconds", options.url, timeout)
1587
1531
 
1588
1532
  await anyio.sleep(timeout)
1589
1533
 
1590
- return await self._request(
1591
- options=options,
1592
- cast_to=cast_to,
1593
- retries_taken=retries_taken + 1,
1594
- stream=stream,
1595
- stream_cls=stream_cls,
1596
- )
1597
-
1598
1534
  async def _process_response(
1599
1535
  self,
1600
1536
  *,