spitch 1.25.0__py3-none-any.whl → 1.26.0__py3-none-any.whl

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.

Potentially problematic release.


This version of spitch might be problematic. Click here for more details.

spitch/_base_client.py CHANGED
@@ -98,7 +98,11 @@ _StreamT = TypeVar("_StreamT", bound=Stream[Any])
98
98
  _AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any])
99
99
 
100
100
  if TYPE_CHECKING:
101
- from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT
101
+ from httpx._config import (
102
+ DEFAULT_TIMEOUT_CONFIG, # pyright: ignore[reportPrivateImportUsage]
103
+ )
104
+
105
+ HTTPX_DEFAULT_TIMEOUT = DEFAULT_TIMEOUT_CONFIG
102
106
  else:
103
107
  try:
104
108
  from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT
@@ -115,6 +119,7 @@ class PageInfo:
115
119
 
116
120
  url: URL | NotGiven
117
121
  params: Query | NotGiven
122
+ json: Body | NotGiven
118
123
 
119
124
  @overload
120
125
  def __init__(
@@ -130,19 +135,30 @@ class PageInfo:
130
135
  params: Query,
131
136
  ) -> None: ...
132
137
 
138
+ @overload
139
+ def __init__(
140
+ self,
141
+ *,
142
+ json: Body,
143
+ ) -> None: ...
144
+
133
145
  def __init__(
134
146
  self,
135
147
  *,
136
148
  url: URL | NotGiven = NOT_GIVEN,
149
+ json: Body | NotGiven = NOT_GIVEN,
137
150
  params: Query | NotGiven = NOT_GIVEN,
138
151
  ) -> None:
139
152
  self.url = url
153
+ self.json = json
140
154
  self.params = params
141
155
 
142
156
  @override
143
157
  def __repr__(self) -> str:
144
158
  if self.url:
145
159
  return f"{self.__class__.__name__}(url={self.url})"
160
+ if self.json:
161
+ return f"{self.__class__.__name__}(json={self.json})"
146
162
  return f"{self.__class__.__name__}(params={self.params})"
147
163
 
148
164
 
@@ -191,6 +207,19 @@ class BasePage(GenericModel, Generic[_T]):
191
207
  options.url = str(url)
192
208
  return options
193
209
 
210
+ if not isinstance(info.json, NotGiven):
211
+ if not is_mapping(info.json):
212
+ raise TypeError("Pagination is only supported with mappings")
213
+
214
+ if not options.json_data:
215
+ options.json_data = {**info.json}
216
+ else:
217
+ if not is_mapping(options.json_data):
218
+ raise TypeError("Pagination is only supported with mappings")
219
+
220
+ options.json_data = {**options.json_data, **info.json}
221
+ return options
222
+
194
223
  raise ValueError("Unexpected PageInfo state")
195
224
 
196
225
 
@@ -408,8 +437,8 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
408
437
  headers = httpx.Headers(headers_dict)
409
438
 
410
439
  idempotency_header = self._idempotency_header
411
- if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers:
412
- headers[idempotency_header] = options.idempotency_key or self._idempotency_key()
440
+ if idempotency_header and options.idempotency_key and idempotency_header not in headers:
441
+ headers[idempotency_header] = options.idempotency_key
413
442
 
414
443
  # Don't set these headers if they were already set or removed by the caller. We check
415
444
  # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case.
@@ -873,7 +902,6 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
873
902
  self,
874
903
  cast_to: Type[ResponseT],
875
904
  options: FinalRequestOptions,
876
- remaining_retries: Optional[int] = None,
877
905
  *,
878
906
  stream: Literal[True],
879
907
  stream_cls: Type[_StreamT],
@@ -884,7 +912,6 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
884
912
  self,
885
913
  cast_to: Type[ResponseT],
886
914
  options: FinalRequestOptions,
887
- remaining_retries: Optional[int] = None,
888
915
  *,
889
916
  stream: Literal[False] = False,
890
917
  ) -> ResponseT: ...
@@ -894,7 +921,6 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
894
921
  self,
895
922
  cast_to: Type[ResponseT],
896
923
  options: FinalRequestOptions,
897
- remaining_retries: Optional[int] = None,
898
924
  *,
899
925
  stream: bool = False,
900
926
  stream_cls: Type[_StreamT] | None = None,
@@ -904,121 +930,109 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
904
930
  self,
905
931
  cast_to: Type[ResponseT],
906
932
  options: FinalRequestOptions,
907
- remaining_retries: Optional[int] = None,
908
933
  *,
909
934
  stream: bool = False,
910
935
  stream_cls: type[_StreamT] | None = None,
911
936
  ) -> ResponseT | _StreamT:
912
- if remaining_retries is not None:
913
- retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
914
- else:
915
- retries_taken = 0
916
-
917
- return self._request(
918
- cast_to=cast_to,
919
- options=options,
920
- stream=stream,
921
- stream_cls=stream_cls,
922
- retries_taken=retries_taken,
923
- )
937
+ cast_to = self._maybe_override_cast_to(cast_to, options)
924
938
 
925
- def _request(
926
- self,
927
- *,
928
- cast_to: Type[ResponseT],
929
- options: FinalRequestOptions,
930
- retries_taken: int,
931
- stream: bool,
932
- stream_cls: type[_StreamT] | None,
933
- ) -> ResponseT | _StreamT:
934
939
  # create a copy of the options we were given so that if the
935
940
  # options are mutated later & we then retry, the retries are
936
941
  # given the original options
937
942
  input_options = model_copy(options)
943
+ if input_options.idempotency_key is None and input_options.method.lower() != "get":
944
+ # ensure the idempotency key is reused between requests
945
+ input_options.idempotency_key = self._idempotency_key()
938
946
 
939
- cast_to = self._maybe_override_cast_to(cast_to, options)
940
- options = self._prepare_options(options)
941
-
942
- remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
943
- request = self._build_request(options, retries_taken=retries_taken)
944
- self._prepare_request(request)
945
-
946
- kwargs: HttpxSendArgs = {}
947
- if self.custom_auth is not None:
948
- kwargs["auth"] = self.custom_auth
947
+ response: httpx.Response | None = None
948
+ max_retries = input_options.get_max_retries(self.max_retries)
949
949
 
950
- log.debug("Sending HTTP Request: %s %s", request.method, request.url)
950
+ retries_taken = 0
951
+ for retries_taken in range(max_retries + 1):
952
+ options = model_copy(input_options)
953
+ options = self._prepare_options(options)
951
954
 
952
- try:
953
- response = self._client.send(
954
- request,
955
- stream=stream or self._should_stream_response_body(request=request),
956
- **kwargs,
957
- )
958
- except httpx.TimeoutException as err:
959
- log.debug("Encountered httpx.TimeoutException", exc_info=True)
955
+ remaining_retries = max_retries - retries_taken
956
+ request = self._build_request(options, retries_taken=retries_taken)
957
+ self._prepare_request(request)
960
958
 
961
- if remaining_retries > 0:
962
- return self._retry_request(
963
- input_options,
964
- cast_to,
965
- retries_taken=retries_taken,
966
- stream=stream,
967
- stream_cls=stream_cls,
968
- response_headers=None,
969
- )
959
+ kwargs: HttpxSendArgs = {}
960
+ if self.custom_auth is not None:
961
+ kwargs["auth"] = self.custom_auth
970
962
 
971
- log.debug("Raising timeout error")
972
- raise APITimeoutError(request=request) from err
973
- except Exception as err:
974
- log.debug("Encountered Exception", exc_info=True)
963
+ log.debug("Sending HTTP Request: %s %s", request.method, request.url)
975
964
 
976
- if remaining_retries > 0:
977
- return self._retry_request(
978
- input_options,
979
- cast_to,
980
- retries_taken=retries_taken,
981
- stream=stream,
982
- stream_cls=stream_cls,
983
- response_headers=None,
965
+ response = None
966
+ try:
967
+ response = self._client.send(
968
+ request,
969
+ stream=stream or self._should_stream_response_body(request=request),
970
+ **kwargs,
984
971
  )
972
+ except httpx.TimeoutException as err:
973
+ log.debug("Encountered httpx.TimeoutException", exc_info=True)
974
+
975
+ if remaining_retries > 0:
976
+ self._sleep_for_retry(
977
+ retries_taken=retries_taken,
978
+ max_retries=max_retries,
979
+ options=input_options,
980
+ response=None,
981
+ )
982
+ continue
983
+
984
+ log.debug("Raising timeout error")
985
+ raise APITimeoutError(request=request) from err
986
+ except Exception as err:
987
+ log.debug("Encountered Exception", exc_info=True)
988
+
989
+ if remaining_retries > 0:
990
+ self._sleep_for_retry(
991
+ retries_taken=retries_taken,
992
+ max_retries=max_retries,
993
+ options=input_options,
994
+ response=None,
995
+ )
996
+ continue
997
+
998
+ log.debug("Raising connection error")
999
+ raise APIConnectionError(request=request) from err
1000
+
1001
+ log.debug(
1002
+ 'HTTP Response: %s %s "%i %s" %s',
1003
+ request.method,
1004
+ request.url,
1005
+ response.status_code,
1006
+ response.reason_phrase,
1007
+ response.headers,
1008
+ )
985
1009
 
986
- log.debug("Raising connection error")
987
- raise APIConnectionError(request=request) from err
988
-
989
- log.debug(
990
- 'HTTP Response: %s %s "%i %s" %s',
991
- request.method,
992
- request.url,
993
- response.status_code,
994
- response.reason_phrase,
995
- response.headers,
996
- )
1010
+ try:
1011
+ response.raise_for_status()
1012
+ except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
1013
+ log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
1014
+
1015
+ if remaining_retries > 0 and self._should_retry(err.response):
1016
+ err.response.close()
1017
+ self._sleep_for_retry(
1018
+ retries_taken=retries_taken,
1019
+ max_retries=max_retries,
1020
+ options=input_options,
1021
+ response=response,
1022
+ )
1023
+ continue
997
1024
 
998
- try:
999
- response.raise_for_status()
1000
- except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
1001
- log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
1002
-
1003
- if remaining_retries > 0 and self._should_retry(err.response):
1004
- err.response.close()
1005
- return self._retry_request(
1006
- input_options,
1007
- cast_to,
1008
- retries_taken=retries_taken,
1009
- response_headers=err.response.headers,
1010
- stream=stream,
1011
- stream_cls=stream_cls,
1012
- )
1025
+ # If the response is streamed then we need to explicitly read the response
1026
+ # to completion before attempting to access the response text.
1027
+ if not err.response.is_closed:
1028
+ err.response.read()
1013
1029
 
1014
- # If the response is streamed then we need to explicitly read the response
1015
- # to completion before attempting to access the response text.
1016
- if not err.response.is_closed:
1017
- err.response.read()
1030
+ log.debug("Re-raising status error")
1031
+ raise self._make_status_error_from_response(err.response) from None
1018
1032
 
1019
- log.debug("Re-raising status error")
1020
- raise self._make_status_error_from_response(err.response) from None
1033
+ break
1021
1034
 
1035
+ assert response is not None, "could not resolve response (should never happen)"
1022
1036
  return self._process_response(
1023
1037
  cast_to=cast_to,
1024
1038
  options=options,
@@ -1028,37 +1042,20 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
1028
1042
  retries_taken=retries_taken,
1029
1043
  )
1030
1044
 
1031
- def _retry_request(
1032
- self,
1033
- options: FinalRequestOptions,
1034
- cast_to: Type[ResponseT],
1035
- *,
1036
- retries_taken: int,
1037
- response_headers: httpx.Headers | None,
1038
- stream: bool,
1039
- stream_cls: type[_StreamT] | None,
1040
- ) -> ResponseT | _StreamT:
1041
- remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
1045
+ def _sleep_for_retry(
1046
+ self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None
1047
+ ) -> None:
1048
+ remaining_retries = max_retries - retries_taken
1042
1049
  if remaining_retries == 1:
1043
1050
  log.debug("1 retry left")
1044
1051
  else:
1045
1052
  log.debug("%i retries left", remaining_retries)
1046
1053
 
1047
- timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
1054
+ timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None)
1048
1055
  log.info("Retrying request to %s in %f seconds", options.url, timeout)
1049
1056
 
1050
- # In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a
1051
- # different thread if necessary.
1052
1057
  time.sleep(timeout)
1053
1058
 
1054
- return self._request(
1055
- options=options,
1056
- cast_to=cast_to,
1057
- retries_taken=retries_taken + 1,
1058
- stream=stream,
1059
- stream_cls=stream_cls,
1060
- )
1061
-
1062
1059
  def _process_response(
1063
1060
  self,
1064
1061
  *,
@@ -1406,7 +1403,6 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1406
1403
  options: FinalRequestOptions,
1407
1404
  *,
1408
1405
  stream: Literal[False] = False,
1409
- remaining_retries: Optional[int] = None,
1410
1406
  ) -> ResponseT: ...
1411
1407
 
1412
1408
  @overload
@@ -1417,7 +1413,6 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1417
1413
  *,
1418
1414
  stream: Literal[True],
1419
1415
  stream_cls: type[_AsyncStreamT],
1420
- remaining_retries: Optional[int] = None,
1421
1416
  ) -> _AsyncStreamT: ...
1422
1417
 
1423
1418
  @overload
@@ -1428,7 +1423,6 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1428
1423
  *,
1429
1424
  stream: bool,
1430
1425
  stream_cls: type[_AsyncStreamT] | None = None,
1431
- remaining_retries: Optional[int] = None,
1432
1426
  ) -> ResponseT | _AsyncStreamT: ...
1433
1427
 
1434
1428
  async def request(
@@ -1438,116 +1432,111 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1438
1432
  *,
1439
1433
  stream: bool = False,
1440
1434
  stream_cls: type[_AsyncStreamT] | None = None,
1441
- remaining_retries: Optional[int] = None,
1442
- ) -> ResponseT | _AsyncStreamT:
1443
- if remaining_retries is not None:
1444
- retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
1445
- else:
1446
- retries_taken = 0
1447
-
1448
- return await self._request(
1449
- cast_to=cast_to,
1450
- options=options,
1451
- stream=stream,
1452
- stream_cls=stream_cls,
1453
- retries_taken=retries_taken,
1454
- )
1455
-
1456
- async def _request(
1457
- self,
1458
- cast_to: Type[ResponseT],
1459
- options: FinalRequestOptions,
1460
- *,
1461
- stream: bool,
1462
- stream_cls: type[_AsyncStreamT] | None,
1463
- retries_taken: int,
1464
1435
  ) -> ResponseT | _AsyncStreamT:
1465
1436
  if self._platform is None:
1466
1437
  # `get_platform` can make blocking IO calls so we
1467
1438
  # execute it earlier while we are in an async context
1468
1439
  self._platform = await asyncify(get_platform)()
1469
1440
 
1441
+ cast_to = self._maybe_override_cast_to(cast_to, options)
1442
+
1470
1443
  # create a copy of the options we were given so that if the
1471
1444
  # options are mutated later & we then retry, the retries are
1472
1445
  # given the original options
1473
1446
  input_options = model_copy(options)
1447
+ if input_options.idempotency_key is None and input_options.method.lower() != "get":
1448
+ # ensure the idempotency key is reused between requests
1449
+ input_options.idempotency_key = self._idempotency_key()
1474
1450
 
1475
- cast_to = self._maybe_override_cast_to(cast_to, options)
1476
- options = await self._prepare_options(options)
1451
+ response: httpx.Response | None = None
1452
+ max_retries = input_options.get_max_retries(self.max_retries)
1477
1453
 
1478
- remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
1479
- request = self._build_request(options, retries_taken=retries_taken)
1480
- await self._prepare_request(request)
1454
+ retries_taken = 0
1455
+ for retries_taken in range(max_retries + 1):
1456
+ options = model_copy(input_options)
1457
+ options = await self._prepare_options(options)
1481
1458
 
1482
- kwargs: HttpxSendArgs = {}
1483
- if self.custom_auth is not None:
1484
- kwargs["auth"] = self.custom_auth
1459
+ remaining_retries = max_retries - retries_taken
1460
+ request = self._build_request(options, retries_taken=retries_taken)
1461
+ await self._prepare_request(request)
1485
1462
 
1486
- try:
1487
- response = await self._client.send(
1488
- request,
1489
- stream=stream or self._should_stream_response_body(request=request),
1490
- **kwargs,
1491
- )
1492
- except httpx.TimeoutException as err:
1493
- log.debug("Encountered httpx.TimeoutException", exc_info=True)
1494
-
1495
- if remaining_retries > 0:
1496
- return await self._retry_request(
1497
- input_options,
1498
- cast_to,
1499
- retries_taken=retries_taken,
1500
- stream=stream,
1501
- stream_cls=stream_cls,
1502
- response_headers=None,
1503
- )
1463
+ kwargs: HttpxSendArgs = {}
1464
+ if self.custom_auth is not None:
1465
+ kwargs["auth"] = self.custom_auth
1504
1466
 
1505
- log.debug("Raising timeout error")
1506
- raise APITimeoutError(request=request) from err
1507
- except Exception as err:
1508
- log.debug("Encountered Exception", exc_info=True)
1467
+ log.debug("Sending HTTP Request: %s %s", request.method, request.url)
1509
1468
 
1510
- if remaining_retries > 0:
1511
- return await self._retry_request(
1512
- input_options,
1513
- cast_to,
1514
- retries_taken=retries_taken,
1515
- stream=stream,
1516
- stream_cls=stream_cls,
1517
- response_headers=None,
1469
+ response = None
1470
+ try:
1471
+ response = await self._client.send(
1472
+ request,
1473
+ stream=stream or self._should_stream_response_body(request=request),
1474
+ **kwargs,
1518
1475
  )
1476
+ except httpx.TimeoutException as err:
1477
+ log.debug("Encountered httpx.TimeoutException", exc_info=True)
1478
+
1479
+ if remaining_retries > 0:
1480
+ await self._sleep_for_retry(
1481
+ retries_taken=retries_taken,
1482
+ max_retries=max_retries,
1483
+ options=input_options,
1484
+ response=None,
1485
+ )
1486
+ continue
1487
+
1488
+ log.debug("Raising timeout error")
1489
+ raise APITimeoutError(request=request) from err
1490
+ except Exception as err:
1491
+ log.debug("Encountered Exception", exc_info=True)
1492
+
1493
+ if remaining_retries > 0:
1494
+ await self._sleep_for_retry(
1495
+ retries_taken=retries_taken,
1496
+ max_retries=max_retries,
1497
+ options=input_options,
1498
+ response=None,
1499
+ )
1500
+ continue
1501
+
1502
+ log.debug("Raising connection error")
1503
+ raise APIConnectionError(request=request) from err
1504
+
1505
+ log.debug(
1506
+ 'HTTP Response: %s %s "%i %s" %s',
1507
+ request.method,
1508
+ request.url,
1509
+ response.status_code,
1510
+ response.reason_phrase,
1511
+ response.headers,
1512
+ )
1519
1513
 
1520
- log.debug("Raising connection error")
1521
- raise APIConnectionError(request=request) from err
1514
+ try:
1515
+ response.raise_for_status()
1516
+ except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
1517
+ log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
1518
+
1519
+ if remaining_retries > 0 and self._should_retry(err.response):
1520
+ await err.response.aclose()
1521
+ await self._sleep_for_retry(
1522
+ retries_taken=retries_taken,
1523
+ max_retries=max_retries,
1524
+ options=input_options,
1525
+ response=response,
1526
+ )
1527
+ continue
1522
1528
 
1523
- log.debug(
1524
- 'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase
1525
- )
1529
+ # If the response is streamed then we need to explicitly read the response
1530
+ # to completion before attempting to access the response text.
1531
+ if not err.response.is_closed:
1532
+ await err.response.aread()
1526
1533
 
1527
- try:
1528
- response.raise_for_status()
1529
- except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
1530
- log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
1531
-
1532
- if remaining_retries > 0 and self._should_retry(err.response):
1533
- await err.response.aclose()
1534
- return await self._retry_request(
1535
- input_options,
1536
- cast_to,
1537
- retries_taken=retries_taken,
1538
- response_headers=err.response.headers,
1539
- stream=stream,
1540
- stream_cls=stream_cls,
1541
- )
1534
+ log.debug("Re-raising status error")
1535
+ raise self._make_status_error_from_response(err.response) from None
1542
1536
 
1543
- # If the response is streamed then we need to explicitly read the response
1544
- # to completion before attempting to access the response text.
1545
- if not err.response.is_closed:
1546
- await err.response.aread()
1547
-
1548
- log.debug("Re-raising status error")
1549
- raise self._make_status_error_from_response(err.response) from None
1537
+ break
1550
1538
 
1539
+ assert response is not None, "could not resolve response (should never happen)"
1551
1540
  return await self._process_response(
1552
1541
  cast_to=cast_to,
1553
1542
  options=options,
@@ -1557,35 +1546,20 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1557
1546
  retries_taken=retries_taken,
1558
1547
  )
1559
1548
 
1560
- async def _retry_request(
1561
- self,
1562
- options: FinalRequestOptions,
1563
- cast_to: Type[ResponseT],
1564
- *,
1565
- retries_taken: int,
1566
- response_headers: httpx.Headers | None,
1567
- stream: bool,
1568
- stream_cls: type[_AsyncStreamT] | None,
1569
- ) -> ResponseT | _AsyncStreamT:
1570
- remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
1549
+ async def _sleep_for_retry(
1550
+ self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None
1551
+ ) -> None:
1552
+ remaining_retries = max_retries - retries_taken
1571
1553
  if remaining_retries == 1:
1572
1554
  log.debug("1 retry left")
1573
1555
  else:
1574
1556
  log.debug("%i retries left", remaining_retries)
1575
1557
 
1576
- timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
1558
+ timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None)
1577
1559
  log.info("Retrying request to %s in %f seconds", options.url, timeout)
1578
1560
 
1579
1561
  await anyio.sleep(timeout)
1580
1562
 
1581
- return await self._request(
1582
- options=options,
1583
- cast_to=cast_to,
1584
- retries_taken=retries_taken + 1,
1585
- stream=stream,
1586
- stream_cls=stream_cls,
1587
- )
1588
-
1589
1563
  async def _process_response(
1590
1564
  self,
1591
1565
  *,
spitch/_client.py CHANGED
@@ -19,10 +19,7 @@ from ._types import (
19
19
  ProxiesTypes,
20
20
  RequestOptions,
21
21
  )
22
- from ._utils import (
23
- is_given,
24
- get_async_library,
25
- )
22
+ from ._utils import is_given, get_async_library
26
23
  from ._version import __version__
27
24
  from ._streaming import Stream as Stream, AsyncStream as AsyncStream
28
25
  from ._exceptions import SpitchError, APIStatusError
spitch/_models.py CHANGED
@@ -19,7 +19,6 @@ from typing_extensions import (
19
19
  )
20
20
 
21
21
  import pydantic
22
- import pydantic.generics
23
22
  from pydantic.fields import FieldInfo
24
23
 
25
24
  from ._types import (
@@ -623,8 +622,8 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
623
622
  # Note: if one variant defines an alias then they all should
624
623
  discriminator_alias = field_info.alias
625
624
 
626
- if field_info.annotation and is_literal_type(field_info.annotation):
627
- for entry in get_args(field_info.annotation):
625
+ if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation):
626
+ for entry in get_args(annotation):
628
627
  if isinstance(entry, str):
629
628
  mapping[entry] = variant
630
629
 
spitch/_utils/_typing.py CHANGED
@@ -81,7 +81,7 @@ def extract_type_var_from_base(
81
81
  ```
82
82
  """
83
83
  cls = cast(object, get_origin(typ) or typ)
84
- if cls in generic_bases:
84
+ if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains]
85
85
  # we're given the class directly
86
86
  return extract_type_arg(typ, index)
87
87
 
spitch/_utils/_utils.py CHANGED
@@ -72,8 +72,16 @@ def _extract_items(
72
72
  from .._files import assert_is_file_content
73
73
 
74
74
  # We have exhausted the path, return the entry we found.
75
- assert_is_file_content(obj, key=flattened_key)
76
75
  assert flattened_key is not None
76
+
77
+ if is_list(obj):
78
+ files: list[tuple[str, FileTypes]] = []
79
+ for entry in obj:
80
+ assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "")
81
+ files.append((flattened_key + "[]", cast(FileTypes, entry)))
82
+ return files
83
+
84
+ assert_is_file_content(obj, key=flattened_key)
77
85
  return [(flattened_key, cast(FileTypes, obj))]
78
86
 
79
87
  index += 1
spitch/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "spitch"
4
- __version__ = "1.25.0" # x-release-please-version
4
+ __version__ = "1.26.0" # x-release-please-version
@@ -9,12 +9,7 @@ import httpx
9
9
 
10
10
  from ..types import speech_generate_params, speech_transcribe_params
11
11
  from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
12
- from .._utils import (
13
- extract_files,
14
- maybe_transform,
15
- deepcopy_minimal,
16
- async_maybe_transform,
17
- )
12
+ from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
18
13
  from .._compat import cached_property
19
14
  from .._resource import SyncAPIResource, AsyncAPIResource
20
15
  from .._response import (
@@ -60,7 +55,7 @@ class SpeechResource(SyncAPIResource):
60
55
  def generate(
61
56
  self,
62
57
  *,
63
- language: Literal["yo", "en", "ha", "ig"],
58
+ language: Literal["yo", "en", "ha", "ig", "am"],
64
59
  text: str,
65
60
  voice: Literal[
66
61
  "sade",
@@ -81,6 +76,10 @@ class SpeechResource(SyncAPIResource):
81
76
  "jude",
82
77
  "henry",
83
78
  "kani",
79
+ "hana",
80
+ "selam",
81
+ "tena",
82
+ "tesfaye",
84
83
  ],
85
84
  stream: bool | NotGiven = NOT_GIVEN,
86
85
  # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -126,7 +125,7 @@ class SpeechResource(SyncAPIResource):
126
125
  def transcribe(
127
126
  self,
128
127
  *,
129
- language: Literal["yo", "en", "ha", "ig"],
128
+ language: Literal["yo", "en", "ha", "ig", "am"],
130
129
  content: Optional[FileTypes] | NotGiven = NOT_GIVEN,
131
130
  multispeaker: Optional[bool] | NotGiven = NOT_GIVEN,
132
131
  timestamp: Optional[bool] | NotGiven = NOT_GIVEN,
@@ -198,7 +197,7 @@ class AsyncSpeechResource(AsyncAPIResource):
198
197
  async def generate(
199
198
  self,
200
199
  *,
201
- language: Literal["yo", "en", "ha", "ig"],
200
+ language: Literal["yo", "en", "ha", "ig", "am"],
202
201
  text: str,
203
202
  voice: Literal[
204
203
  "sade",
@@ -219,6 +218,10 @@ class AsyncSpeechResource(AsyncAPIResource):
219
218
  "jude",
220
219
  "henry",
221
220
  "kani",
221
+ "hana",
222
+ "selam",
223
+ "tena",
224
+ "tesfaye",
222
225
  ],
223
226
  stream: bool | NotGiven = NOT_GIVEN,
224
227
  # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -264,7 +267,7 @@ class AsyncSpeechResource(AsyncAPIResource):
264
267
  async def transcribe(
265
268
  self,
266
269
  *,
267
- language: Literal["yo", "en", "ha", "ig"],
270
+ language: Literal["yo", "en", "ha", "ig", "am"],
268
271
  content: Optional[FileTypes] | NotGiven = NOT_GIVEN,
269
272
  multispeaker: Optional[bool] | NotGiven = NOT_GIVEN,
270
273
  timestamp: Optional[bool] | NotGiven = NOT_GIVEN,
spitch/resources/text.py CHANGED
@@ -8,10 +8,7 @@ import httpx
8
8
 
9
9
  from ..types import text_tone_mark_params, text_translate_params
10
10
  from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven
11
- from .._utils import (
12
- maybe_transform,
13
- async_maybe_transform,
14
- )
11
+ from .._utils import maybe_transform, async_maybe_transform
15
12
  from .._compat import cached_property
16
13
  from .._resource import SyncAPIResource, AsyncAPIResource
17
14
  from .._response import (
@@ -50,7 +47,7 @@ class TextResource(SyncAPIResource):
50
47
  def tone_mark(
51
48
  self,
52
49
  *,
53
- language: Literal["yo", "en", "ha", "ig"],
50
+ language: Literal["yo", "en", "ha", "ig", "am"],
54
51
  text: str,
55
52
  # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
56
53
  # The extra values given here take precedence over values defined on the client or passed to this method.
@@ -89,8 +86,8 @@ class TextResource(SyncAPIResource):
89
86
  def translate(
90
87
  self,
91
88
  *,
92
- source: Literal["yo", "en", "ha", "ig"],
93
- target: Literal["yo", "en", "ha", "ig"],
89
+ source: Literal["yo", "en", "ha", "ig", "am"],
90
+ target: Literal["yo", "en", "ha", "ig", "am"],
94
91
  text: str,
95
92
  # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
96
93
  # The extra values given here take precedence over values defined on the client or passed to this method.
@@ -151,7 +148,7 @@ class AsyncTextResource(AsyncAPIResource):
151
148
  async def tone_mark(
152
149
  self,
153
150
  *,
154
- language: Literal["yo", "en", "ha", "ig"],
151
+ language: Literal["yo", "en", "ha", "ig", "am"],
155
152
  text: str,
156
153
  # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
157
154
  # The extra values given here take precedence over values defined on the client or passed to this method.
@@ -190,8 +187,8 @@ class AsyncTextResource(AsyncAPIResource):
190
187
  async def translate(
191
188
  self,
192
189
  *,
193
- source: Literal["yo", "en", "ha", "ig"],
194
- target: Literal["yo", "en", "ha", "ig"],
190
+ source: Literal["yo", "en", "ha", "ig", "am"],
191
+ target: Literal["yo", "en", "ha", "ig", "am"],
195
192
  text: str,
196
193
  # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
197
194
  # The extra values given here take precedence over values defined on the client or passed to this method.
@@ -8,7 +8,7 @@ __all__ = ["SpeechGenerateParams"]
8
8
 
9
9
 
10
10
  class SpeechGenerateParams(TypedDict, total=False):
11
- language: Required[Literal["yo", "en", "ha", "ig"]]
11
+ language: Required[Literal["yo", "en", "ha", "ig", "am"]]
12
12
 
13
13
  text: Required[str]
14
14
 
@@ -32,6 +32,10 @@ class SpeechGenerateParams(TypedDict, total=False):
32
32
  "jude",
33
33
  "henry",
34
34
  "kani",
35
+ "hana",
36
+ "selam",
37
+ "tena",
38
+ "tesfaye",
35
39
  ]
36
40
  ]
37
41
 
@@ -11,7 +11,7 @@ __all__ = ["SpeechTranscribeParams"]
11
11
 
12
12
 
13
13
  class SpeechTranscribeParams(TypedDict, total=False):
14
- language: Required[Literal["yo", "en", "ha", "ig"]]
14
+ language: Required[Literal["yo", "en", "ha", "ig", "am"]]
15
15
 
16
16
  content: Optional[FileTypes]
17
17
 
@@ -8,6 +8,6 @@ __all__ = ["TextToneMarkParams"]
8
8
 
9
9
 
10
10
  class TextToneMarkParams(TypedDict, total=False):
11
- language: Required[Literal["yo", "en", "ha", "ig"]]
11
+ language: Required[Literal["yo", "en", "ha", "ig", "am"]]
12
12
 
13
13
  text: Required[str]
@@ -8,8 +8,8 @@ __all__ = ["TextTranslateParams"]
8
8
 
9
9
 
10
10
  class TextTranslateParams(TypedDict, total=False):
11
- source: Required[Literal["yo", "en", "ha", "ig"]]
11
+ source: Required[Literal["yo", "en", "ha", "ig", "am"]]
12
12
 
13
- target: Required[Literal["yo", "en", "ha", "ig"]]
13
+ target: Required[Literal["yo", "en", "ha", "ig", "am"]]
14
14
 
15
15
  text: Required[str]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: spitch
3
- Version: 1.25.0
3
+ Version: 1.26.0
4
4
  Summary: The official Python library for the spitch API
5
5
  Project-URL: Homepage, https://github.com/spi-tch/spitch-python
6
6
  Project-URL: Repository, https://github.com/spi-tch/spitch-python
@@ -42,7 +42,7 @@ It is generated with [Stainless](https://www.stainless.com/).
42
42
 
43
43
  ## Documentation
44
44
 
45
- The REST API documentation can be found on [docs.spi-tch.com](https://docs.spi-tch.com). The full API of this library can be found in [api.md](https://github.com/spi-tch/spitch-python/tree/main/api.md).
45
+ The REST API documentation can be found on [docs.spitch.app](https://docs.spitch.app). The full API of this library can be found in [api.md](https://github.com/spi-tch/spitch-python/tree/main/api.md).
46
46
 
47
47
  ## Installation
48
48
 
@@ -1,17 +1,17 @@
1
1
  spitch/__init__.py,sha256=mBQhmNu88PeR66Zmw9atnzuU8f1mpSPcW7vn-EeSikE,2399
2
- spitch/_base_client.py,sha256=oORuab7liaQOg-Ud7teTgIfnBZVflIa8LfQA4BWTyog,65086
3
- spitch/_client.py,sha256=53FDDffn_8d-95rmM7-awLEUo9h_K2563fOmQm6qL-I,15451
2
+ spitch/_base_client.py,sha256=eF0diFIgeVERwrLpcaCFFha0xTFqvX6yqX-CP9oEiNk,64973
3
+ spitch/_client.py,sha256=edWlodXy44ArTu9KY7M_zXYKMhs66gptWUqSuMwN0SI,15438
4
4
  spitch/_compat.py,sha256=fQkXUY7reJc8m_yguMWSjHBfO8lNzw4wOAxtkhP9d1Q,6607
5
5
  spitch/_constants.py,sha256=S14PFzyN9-I31wiV7SmIlL5Ga0MLHxdvegInGdXH7tM,462
6
6
  spitch/_exceptions.py,sha256=xsQtKJTiIdz2X1bQDQFZcSW7WBofLazdQm9nMCyPEVM,3220
7
7
  spitch/_files.py,sha256=wV8OmI8oHeNVRtF-7aAEu22jtRG4FzjOioE8lBp-jNA,3617
8
- spitch/_models.py,sha256=hss4ZeAKZvg4pgBAlpn4iQ-IEERaInyjuGS3Nr8y5Rw,28892
8
+ spitch/_models.py,sha256=wlKYVUQAgnHUb4uSfD3yNW6rt8SMeFKmlEf8wo7VYs0,28879
9
9
  spitch/_qs.py,sha256=AOkSz4rHtK4YI3ZU_kzea-zpwBUgEY8WniGmTPyEimc,4846
10
10
  spitch/_resource.py,sha256=TLFPcOOmtxZOQLh3XCNPB_BdrQpp0MIYoKoH52aRAu8,1100
11
11
  spitch/_response.py,sha256=3pGMe_eI_h4UPlyp8v6Xn_hu3Lv2i8KGnmKGZ1ZlMyc,28691
12
12
  spitch/_streaming.py,sha256=5SpId2EIfF8Ee8UUYmJxqgHUGP1ZdHCUHhHCdNJREFA,10100
13
13
  spitch/_types.py,sha256=uuSZot9wXgdAMJzfF3raLmt3DvhThG7skqUC98_Dm1k,6167
14
- spitch/_version.py,sha256=ocswzkp59gFl77_040xvhERS-5cf1enlNPeIbnISDs4,159
14
+ spitch/_version.py,sha256=u5PYyfbK7cFgrd5yZix1IdEeJ7-BpGGpMLLxlY9aep8,159
15
15
  spitch/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  spitch/_utils/__init__.py,sha256=k266EatJr88V8Zseb7xUimTlCeno9SynRfLwadHP1b4,2016
17
17
  spitch/_utils/_logs.py,sha256=ApRyYK_WgZfEr_ygBUBXWMlTgeMr2tdNOGlH8jE4oJc,774
@@ -20,21 +20,21 @@ spitch/_utils/_reflection.py,sha256=ZmGkIgT_PuwedyNBrrKGbxoWtkpytJNU1uU4QHnmEMU,
20
20
  spitch/_utils/_streams.py,sha256=SMC90diFFecpEg_zgDRVbdR3hSEIgVVij4taD-noMLM,289
21
21
  spitch/_utils/_sync.py,sha256=TpGLrrhRNWTJtODNE6Fup3_k7zrWm1j2RlirzBwre-0,2862
22
22
  spitch/_utils/_transform.py,sha256=n7kskEWz6o__aoNvhFoGVyDoalNe6mJwp-g7BWkdj88,15617
23
- spitch/_utils/_typing.py,sha256=r5qmf4o7lLpjnklQ9UnQY6op6ipr7qq1GyLroPalH7g,3893
24
- spitch/_utils/_utils.py,sha256=8UmbPOy_AAr2uUjjFui-VZSrVBHRj6bfNEKRp5YZP2A,12004
23
+ spitch/_utils/_typing.py,sha256=9UuSEnmE7dgm1SG45Mt-ga1sBhnvZHpq3f2dXbhW1NM,3939
24
+ spitch/_utils/_utils.py,sha256=ts4CiiuNpFiGB6YMdkQRh2SZvYvsl7mAF-JWHCcLDf4,12312
25
25
  spitch/lib/.keep,sha256=wuNrz-5SXo3jJaJOJgz4vFHM41YH_g20F5cRQo0vLes,224
26
26
  spitch/resources/__init__.py,sha256=KT6rAvIlWHQk9QdM4Jp8ABziKILaBrrtiO7LCB5Wa5E,976
27
- spitch/resources/speech.py,sha256=LpI8rtw6xCuqEM_xBec9_owQAsPDRfeqH4azPzwnYWU,13011
28
- spitch/resources/text.py,sha256=0C2v6i5nawuwHkPbUo-J0RSrmMvAbKPmCPVwuLQTJmY,9657
27
+ spitch/resources/speech.py,sha256=wk-ZaTFNfiRVzdm5qOJd-MT88xoC39PWW9XVaAIV_Tw,13182
28
+ spitch/resources/text.py,sha256=wWIE7uyvthKfhkOtoodTBDv_0s6YgWgAqxwICquDSyo,9680
29
29
  spitch/types/__init__.py,sha256=xBZbzXwV3WlBdawp4Mb4IqoQG4VfaLbi-4FMqPbwqlE,704
30
- spitch/types/speech_generate_params.py,sha256=dBCEBNvo1f08VfT3zutoUEHwYGe5mSrr3-6qipTGJ_Y,799
31
- spitch/types/speech_transcribe_params.py,sha256=2nUgsYIURWTKx8qBYE0WfIHh6hXVcdgCz-UC1wIaXpA,515
30
+ spitch/types/speech_generate_params.py,sha256=u3rRJsJLrcw1FlJ-s_egy4-fYT8iz_jG5wbFho5xGjE,889
31
+ spitch/types/speech_transcribe_params.py,sha256=gEVjDl4T80QCE6C8zRmLSs0Addt0NlAwPENNYgkG4tQ,521
32
32
  spitch/types/speech_transcribe_response.py,sha256=mGNIIK-A_YOTi78tlYMRCKykPiFjSWm4gsRjPk1IvF8,516
33
- spitch/types/text_tone_mark_params.py,sha256=63P5VElxanYkDP1ZLEuQt97JSgVpMCaAo4WRWLDvhlY,349
33
+ spitch/types/text_tone_mark_params.py,sha256=MEnWzcSjPT_Z_8Mio96LgwYbx2BngG5By-MoeSt0Sms,355
34
34
  spitch/types/text_tone_mark_response.py,sha256=WGxZsBxLceZ03VM5dafZshp6azdDxpNHcJHhBX7A5DY,277
35
- spitch/types/text_translate_params.py,sha256=rU5hbTyBaP5L_akmgswHoNLcTKWN0Gz1kL1yt3EQL44,404
35
+ spitch/types/text_translate_params.py,sha256=skEeG7oTZUSl_gugnqL4Mb7HE_pEFhKNygZPTvci2hA,416
36
36
  spitch/types/text_translate_response.py,sha256=Az3QSpvarlCNTiB7uVzMH21YoWHWJMBEvgdKgVJZW4M,279
37
- spitch-1.25.0.dist-info/METADATA,sha256=S7-_2GNwJJJiBmJZZed3UIjekuCqxNkWpJkzvmNCDbQ,13255
38
- spitch-1.25.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
39
- spitch-1.25.0.dist-info/licenses/LICENSE,sha256=C0lDWY-no8IxmnqzQA9BA7Z8jeh_bogVPfeWSgeDDcc,11336
40
- spitch-1.25.0.dist-info/RECORD,,
37
+ spitch-1.26.0.dist-info/METADATA,sha256=XS-Z9iO_g1fbE6oyt6-BbbRzBiEZ3jC2J-pJyLEtCzY,13253
38
+ spitch-1.26.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
39
+ spitch-1.26.0.dist-info/licenses/LICENSE,sha256=C0lDWY-no8IxmnqzQA9BA7Z8jeh_bogVPfeWSgeDDcc,11336
40
+ spitch-1.26.0.dist-info/RECORD,,