cartesia 1.4.0__py3-none-any.whl → 2.0.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.
Files changed (181) hide show
  1. cartesia/__init__.py +302 -3
  2. cartesia/api_status/__init__.py +6 -0
  3. cartesia/api_status/client.py +104 -0
  4. cartesia/api_status/requests/__init__.py +5 -0
  5. cartesia/api_status/requests/api_info.py +8 -0
  6. cartesia/api_status/types/__init__.py +5 -0
  7. cartesia/api_status/types/api_info.py +20 -0
  8. cartesia/base_client.py +156 -0
  9. cartesia/client.py +163 -40
  10. cartesia/core/__init__.py +50 -0
  11. cartesia/core/api_error.py +15 -0
  12. cartesia/core/client_wrapper.py +55 -0
  13. cartesia/core/datetime_utils.py +28 -0
  14. cartesia/core/file.py +67 -0
  15. cartesia/core/http_client.py +499 -0
  16. cartesia/core/jsonable_encoder.py +101 -0
  17. cartesia/core/pagination.py +88 -0
  18. cartesia/core/pydantic_utilities.py +296 -0
  19. cartesia/core/query_encoder.py +58 -0
  20. cartesia/core/remove_none_from_dict.py +11 -0
  21. cartesia/core/request_options.py +35 -0
  22. cartesia/core/serialization.py +272 -0
  23. cartesia/datasets/__init__.py +24 -0
  24. cartesia/datasets/requests/__init__.py +15 -0
  25. cartesia/datasets/requests/create_dataset_request.py +7 -0
  26. cartesia/datasets/requests/dataset.py +9 -0
  27. cartesia/datasets/requests/dataset_file.py +9 -0
  28. cartesia/datasets/requests/paginated_dataset_files.py +10 -0
  29. cartesia/datasets/requests/paginated_datasets.py +10 -0
  30. cartesia/datasets/types/__init__.py +17 -0
  31. cartesia/datasets/types/create_dataset_request.py +19 -0
  32. cartesia/datasets/types/dataset.py +21 -0
  33. cartesia/datasets/types/dataset_file.py +21 -0
  34. cartesia/datasets/types/file_purpose.py +5 -0
  35. cartesia/datasets/types/paginated_dataset_files.py +21 -0
  36. cartesia/datasets/types/paginated_datasets.py +21 -0
  37. cartesia/embedding/__init__.py +5 -0
  38. cartesia/embedding/types/__init__.py +5 -0
  39. cartesia/embedding/types/embedding.py +201 -0
  40. cartesia/environment.py +7 -0
  41. cartesia/infill/__init__.py +2 -0
  42. cartesia/infill/client.py +318 -0
  43. cartesia/tts/__init__.py +167 -0
  44. cartesia/{_async_websocket.py → tts/_async_websocket.py} +212 -85
  45. cartesia/tts/_websocket.py +479 -0
  46. cartesia/tts/client.py +407 -0
  47. cartesia/tts/requests/__init__.py +76 -0
  48. cartesia/tts/requests/cancel_context_request.py +17 -0
  49. cartesia/tts/requests/controls.py +11 -0
  50. cartesia/tts/requests/generation_request.py +58 -0
  51. cartesia/tts/requests/mp_3_output_format.py +11 -0
  52. cartesia/tts/requests/output_format.py +30 -0
  53. cartesia/tts/requests/phoneme_timestamps.py +10 -0
  54. cartesia/tts/requests/raw_output_format.py +11 -0
  55. cartesia/tts/requests/speed.py +7 -0
  56. cartesia/tts/requests/tts_request.py +24 -0
  57. cartesia/tts/requests/tts_request_embedding_specifier.py +16 -0
  58. cartesia/tts/requests/tts_request_id_specifier.py +16 -0
  59. cartesia/tts/requests/tts_request_voice_specifier.py +7 -0
  60. cartesia/tts/requests/wav_output_format.py +7 -0
  61. cartesia/tts/requests/web_socket_base_response.py +11 -0
  62. cartesia/tts/requests/web_socket_chunk_response.py +11 -0
  63. cartesia/tts/requests/web_socket_done_response.py +7 -0
  64. cartesia/tts/requests/web_socket_error_response.py +7 -0
  65. cartesia/tts/requests/web_socket_flush_done_response.py +9 -0
  66. cartesia/tts/requests/web_socket_phoneme_timestamps_response.py +9 -0
  67. cartesia/tts/requests/web_socket_raw_output_format.py +11 -0
  68. cartesia/tts/requests/web_socket_request.py +7 -0
  69. cartesia/tts/requests/web_socket_response.py +70 -0
  70. cartesia/tts/requests/web_socket_stream_options.py +8 -0
  71. cartesia/tts/requests/web_socket_timestamps_response.py +9 -0
  72. cartesia/tts/requests/web_socket_tts_output.py +18 -0
  73. cartesia/tts/requests/web_socket_tts_request.py +25 -0
  74. cartesia/tts/requests/word_timestamps.py +10 -0
  75. cartesia/tts/socket_client.py +302 -0
  76. cartesia/tts/types/__init__.py +90 -0
  77. cartesia/tts/types/cancel_context_request.py +28 -0
  78. cartesia/tts/types/context_id.py +3 -0
  79. cartesia/tts/types/controls.py +22 -0
  80. cartesia/tts/types/emotion.py +34 -0
  81. cartesia/tts/types/flush_id.py +3 -0
  82. cartesia/tts/types/generation_request.py +71 -0
  83. cartesia/tts/types/mp_3_output_format.py +23 -0
  84. cartesia/tts/types/natural_specifier.py +5 -0
  85. cartesia/tts/types/numerical_specifier.py +3 -0
  86. cartesia/tts/types/output_format.py +58 -0
  87. cartesia/tts/types/phoneme_timestamps.py +21 -0
  88. cartesia/tts/types/raw_encoding.py +5 -0
  89. cartesia/tts/types/raw_output_format.py +22 -0
  90. cartesia/tts/types/speed.py +7 -0
  91. cartesia/tts/types/supported_language.py +7 -0
  92. cartesia/tts/types/tts_request.py +35 -0
  93. cartesia/tts/types/tts_request_embedding_specifier.py +27 -0
  94. cartesia/tts/types/tts_request_id_specifier.py +27 -0
  95. cartesia/tts/types/tts_request_voice_specifier.py +7 -0
  96. cartesia/tts/types/wav_output_format.py +17 -0
  97. cartesia/tts/types/web_socket_base_response.py +22 -0
  98. cartesia/tts/types/web_socket_chunk_response.py +22 -0
  99. cartesia/tts/types/web_socket_done_response.py +17 -0
  100. cartesia/tts/types/web_socket_error_response.py +19 -0
  101. cartesia/tts/types/web_socket_flush_done_response.py +21 -0
  102. cartesia/tts/types/web_socket_phoneme_timestamps_response.py +20 -0
  103. cartesia/tts/types/web_socket_raw_output_format.py +22 -0
  104. cartesia/tts/types/web_socket_request.py +7 -0
  105. cartesia/tts/types/web_socket_response.py +125 -0
  106. cartesia/tts/types/web_socket_stream_options.py +19 -0
  107. cartesia/tts/types/web_socket_timestamps_response.py +20 -0
  108. cartesia/tts/types/web_socket_tts_output.py +29 -0
  109. cartesia/tts/types/web_socket_tts_request.py +37 -0
  110. cartesia/tts/types/word_timestamps.py +21 -0
  111. cartesia/{_constants.py → tts/utils/constants.py} +2 -2
  112. cartesia/tts/utils/tts.py +64 -0
  113. cartesia/tts/utils/types.py +70 -0
  114. cartesia/version.py +3 -1
  115. cartesia/voice_changer/__init__.py +27 -0
  116. cartesia/voice_changer/client.py +395 -0
  117. cartesia/voice_changer/requests/__init__.py +15 -0
  118. cartesia/voice_changer/requests/streaming_response.py +38 -0
  119. cartesia/voice_changer/types/__init__.py +17 -0
  120. cartesia/voice_changer/types/output_format_container.py +5 -0
  121. cartesia/voice_changer/types/streaming_response.py +64 -0
  122. cartesia/voices/__init__.py +81 -0
  123. cartesia/voices/client.py +1218 -0
  124. cartesia/voices/requests/__init__.py +29 -0
  125. cartesia/voices/requests/create_voice_request.py +23 -0
  126. cartesia/voices/requests/embedding_response.py +8 -0
  127. cartesia/voices/requests/embedding_specifier.py +10 -0
  128. cartesia/voices/requests/get_voices_response.py +24 -0
  129. cartesia/voices/requests/id_specifier.py +10 -0
  130. cartesia/voices/requests/localize_dialect.py +11 -0
  131. cartesia/voices/requests/localize_voice_request.py +28 -0
  132. cartesia/voices/requests/mix_voice_specifier.py +7 -0
  133. cartesia/voices/requests/mix_voices_request.py +9 -0
  134. cartesia/voices/requests/update_voice_request.py +15 -0
  135. cartesia/voices/requests/voice.py +43 -0
  136. cartesia/voices/requests/voice_metadata.py +36 -0
  137. cartesia/voices/types/__init__.py +53 -0
  138. cartesia/voices/types/base_voice_id.py +5 -0
  139. cartesia/voices/types/clone_mode.py +5 -0
  140. cartesia/voices/types/create_voice_request.py +34 -0
  141. cartesia/voices/types/embedding_response.py +20 -0
  142. cartesia/voices/types/embedding_specifier.py +22 -0
  143. cartesia/voices/types/gender.py +5 -0
  144. cartesia/voices/types/gender_presentation.py +5 -0
  145. cartesia/voices/types/get_voices_response.py +34 -0
  146. cartesia/voices/types/id_specifier.py +22 -0
  147. cartesia/voices/types/localize_dialect.py +11 -0
  148. cartesia/voices/types/localize_english_dialect.py +5 -0
  149. cartesia/voices/types/localize_french_dialect.py +5 -0
  150. cartesia/voices/types/localize_portuguese_dialect.py +5 -0
  151. cartesia/voices/types/localize_spanish_dialect.py +5 -0
  152. cartesia/voices/types/localize_target_language.py +7 -0
  153. cartesia/voices/types/localize_voice_request.py +39 -0
  154. cartesia/voices/types/mix_voice_specifier.py +7 -0
  155. cartesia/voices/types/mix_voices_request.py +20 -0
  156. cartesia/voices/types/update_voice_request.py +27 -0
  157. cartesia/voices/types/voice.py +54 -0
  158. cartesia/voices/types/voice_expand_options.py +5 -0
  159. cartesia/voices/types/voice_id.py +3 -0
  160. cartesia/voices/types/voice_metadata.py +48 -0
  161. cartesia/voices/types/weight.py +3 -0
  162. cartesia-2.0.0.dist-info/METADATA +414 -0
  163. cartesia-2.0.0.dist-info/RECORD +165 -0
  164. {cartesia-1.4.0.dist-info → cartesia-2.0.0.dist-info}/WHEEL +1 -1
  165. cartesia/_async_sse.py +0 -95
  166. cartesia/_logger.py +0 -3
  167. cartesia/_sse.py +0 -143
  168. cartesia/_types.py +0 -70
  169. cartesia/_websocket.py +0 -358
  170. cartesia/async_client.py +0 -82
  171. cartesia/async_tts.py +0 -176
  172. cartesia/resource.py +0 -44
  173. cartesia/tts.py +0 -292
  174. cartesia/utils/deprecated.py +0 -55
  175. cartesia/utils/retry.py +0 -87
  176. cartesia/utils/tts.py +0 -78
  177. cartesia/voices.py +0 -204
  178. cartesia-1.4.0.dist-info/METADATA +0 -663
  179. cartesia-1.4.0.dist-info/RECORD +0 -23
  180. cartesia-1.4.0.dist-info/licenses/LICENSE.md +0 -21
  181. /cartesia/{utils/__init__.py → py.typed} +0 -0
@@ -0,0 +1,499 @@
1
+ # This file was auto-generated by Fern from our API Definition.
2
+
3
+ import asyncio
4
+ import email.utils
5
+ import json
6
+ import re
7
+ import time
8
+ import typing
9
+ import urllib.parse
10
+ from contextlib import asynccontextmanager, contextmanager
11
+ from random import random
12
+
13
+ import httpx
14
+
15
+ from .file import File, convert_file_dict_to_httpx_tuples
16
+ from .jsonable_encoder import jsonable_encoder
17
+ from .query_encoder import encode_query
18
+ from .remove_none_from_dict import remove_none_from_dict
19
+ from .request_options import RequestOptions
20
+
21
+ INITIAL_RETRY_DELAY_SECONDS = 0.5
22
+ MAX_RETRY_DELAY_SECONDS = 10
23
+ MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30
24
+
25
+
26
+ def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]:
27
+ """
28
+ This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait.
29
+
30
+ Inspired by the urllib3 retry implementation.
31
+ """
32
+ retry_after_ms = response_headers.get("retry-after-ms")
33
+ if retry_after_ms is not None:
34
+ try:
35
+ return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0
36
+ except Exception:
37
+ pass
38
+
39
+ retry_after = response_headers.get("retry-after")
40
+ if retry_after is None:
41
+ return None
42
+
43
+ # Attempt to parse the header as an int.
44
+ if re.match(r"^\s*[0-9]+\s*$", retry_after):
45
+ seconds = float(retry_after)
46
+ # Fallback to parsing it as a date.
47
+ else:
48
+ retry_date_tuple = email.utils.parsedate_tz(retry_after)
49
+ if retry_date_tuple is None:
50
+ return None
51
+ if retry_date_tuple[9] is None: # Python 2
52
+ # Assume UTC if no timezone was specified
53
+ # On Python2.7, parsedate_tz returns None for a timezone offset
54
+ # instead of 0 if no timezone is given, where mktime_tz treats
55
+ # a None timezone offset as local time.
56
+ retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:]
57
+
58
+ retry_date = email.utils.mktime_tz(retry_date_tuple)
59
+ seconds = retry_date - time.time()
60
+
61
+ if seconds < 0:
62
+ seconds = 0
63
+
64
+ return seconds
65
+
66
+
67
+ def _retry_timeout(response: httpx.Response, retries: int) -> float:
68
+ """
69
+ Determine the amount of time to wait before retrying a request.
70
+ This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff
71
+ with a jitter to determine the number of seconds to wait.
72
+ """
73
+
74
+ # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says.
75
+ retry_after = _parse_retry_after(response.headers)
76
+ if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER:
77
+ return retry_after
78
+
79
+ # Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS.
80
+ retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS)
81
+
82
+ # Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries.
83
+ timeout = retry_delay * (1 - 0.25 * random())
84
+ return timeout if timeout >= 0 else 0
85
+
86
+
87
+ def _should_retry(response: httpx.Response) -> bool:
88
+ retryable_400s = [429, 408, 409]
89
+ return response.status_code >= 500 or response.status_code in retryable_400s
90
+
91
+
92
+ def remove_omit_from_dict(
93
+ original: typing.Dict[str, typing.Optional[typing.Any]],
94
+ omit: typing.Optional[typing.Any],
95
+ ) -> typing.Dict[str, typing.Any]:
96
+ if omit is None:
97
+ return original
98
+ new: typing.Dict[str, typing.Any] = {}
99
+ for key, value in original.items():
100
+ if value is not omit:
101
+ new[key] = value
102
+ return new
103
+
104
+
105
+ def maybe_filter_request_body(
106
+ data: typing.Optional[typing.Any],
107
+ request_options: typing.Optional[RequestOptions],
108
+ omit: typing.Optional[typing.Any],
109
+ ) -> typing.Optional[typing.Any]:
110
+ if data is None:
111
+ return (
112
+ jsonable_encoder(request_options.get("additional_body_parameters", {})) or {}
113
+ if request_options is not None
114
+ else None
115
+ )
116
+ elif not isinstance(data, typing.Mapping):
117
+ data_content = jsonable_encoder(data)
118
+ else:
119
+ data_content = {
120
+ **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore
121
+ **(
122
+ jsonable_encoder(request_options.get("additional_body_parameters", {})) or {}
123
+ if request_options is not None
124
+ else {}
125
+ ),
126
+ }
127
+ return data_content
128
+
129
+
130
+ # Abstracted out for testing purposes
131
+ def get_request_body(
132
+ *,
133
+ json: typing.Optional[typing.Any],
134
+ data: typing.Optional[typing.Any],
135
+ request_options: typing.Optional[RequestOptions],
136
+ omit: typing.Optional[typing.Any],
137
+ ) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]:
138
+ json_body = None
139
+ data_body = None
140
+ if data is not None:
141
+ data_body = maybe_filter_request_body(data, request_options, omit)
142
+ else:
143
+ # If both data and json are None, we send json data in the event extra properties are specified
144
+ json_body = maybe_filter_request_body(json, request_options, omit)
145
+
146
+ # If you have an empty JSON body, you should just send None
147
+ return (json_body if json_body != {} else None), data_body if data_body != {} else None
148
+
149
+
150
+ class HttpClient:
151
+ def __init__(
152
+ self,
153
+ *,
154
+ httpx_client: httpx.Client,
155
+ base_timeout: typing.Callable[[], typing.Optional[float]],
156
+ base_headers: typing.Callable[[], typing.Dict[str, str]],
157
+ base_url: typing.Optional[typing.Callable[[], str]] = None,
158
+ ):
159
+ self.base_url = base_url
160
+ self.base_timeout = base_timeout
161
+ self.base_headers = base_headers
162
+ self.httpx_client = httpx_client
163
+
164
+ def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str:
165
+ base_url = maybe_base_url
166
+ if self.base_url is not None and base_url is None:
167
+ base_url = self.base_url()
168
+
169
+ if base_url is None:
170
+ raise ValueError("A base_url is required to make this request, please provide one and try again.")
171
+ return base_url
172
+
173
+ def request(
174
+ self,
175
+ path: typing.Optional[str] = None,
176
+ *,
177
+ method: str,
178
+ base_url: typing.Optional[str] = None,
179
+ params: typing.Optional[typing.Dict[str, typing.Any]] = None,
180
+ json: typing.Optional[typing.Any] = None,
181
+ data: typing.Optional[typing.Any] = None,
182
+ content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
183
+ files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None,
184
+ headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
185
+ request_options: typing.Optional[RequestOptions] = None,
186
+ retries: int = 0,
187
+ omit: typing.Optional[typing.Any] = None,
188
+ ) -> httpx.Response:
189
+ base_url = self.get_base_url(base_url)
190
+ timeout = (
191
+ request_options.get("timeout_in_seconds")
192
+ if request_options is not None and request_options.get("timeout_in_seconds") is not None
193
+ else self.base_timeout()
194
+ )
195
+
196
+ json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
197
+
198
+ response = self.httpx_client.request(
199
+ method=method,
200
+ url=urllib.parse.urljoin(f"{base_url}/", path),
201
+ headers=jsonable_encoder(
202
+ remove_none_from_dict(
203
+ {
204
+ **self.base_headers(),
205
+ **(headers if headers is not None else {}),
206
+ **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}),
207
+ }
208
+ )
209
+ ),
210
+ params=encode_query(
211
+ jsonable_encoder(
212
+ remove_none_from_dict(
213
+ remove_omit_from_dict(
214
+ {
215
+ **(params if params is not None else {}),
216
+ **(
217
+ request_options.get("additional_query_parameters", {}) or {}
218
+ if request_options is not None
219
+ else {}
220
+ ),
221
+ },
222
+ omit,
223
+ )
224
+ )
225
+ )
226
+ ),
227
+ json=json_body,
228
+ data=data_body,
229
+ content=content,
230
+ files=(
231
+ convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
232
+ if (files is not None and files is not omit)
233
+ else None
234
+ ),
235
+ timeout=timeout,
236
+ )
237
+
238
+ max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0
239
+ if _should_retry(response=response):
240
+ if max_retries > retries:
241
+ time.sleep(_retry_timeout(response=response, retries=retries))
242
+ return self.request(
243
+ path=path,
244
+ method=method,
245
+ base_url=base_url,
246
+ params=params,
247
+ json=json,
248
+ content=content,
249
+ files=files,
250
+ headers=headers,
251
+ request_options=request_options,
252
+ retries=retries + 1,
253
+ omit=omit,
254
+ )
255
+
256
+ return response
257
+
258
+ @contextmanager
259
+ def stream(
260
+ self,
261
+ path: typing.Optional[str] = None,
262
+ *,
263
+ method: str,
264
+ base_url: typing.Optional[str] = None,
265
+ params: typing.Optional[typing.Dict[str, typing.Any]] = None,
266
+ json: typing.Optional[typing.Any] = None,
267
+ data: typing.Optional[typing.Any] = None,
268
+ content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
269
+ files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None,
270
+ headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
271
+ request_options: typing.Optional[RequestOptions] = None,
272
+ retries: int = 0,
273
+ omit: typing.Optional[typing.Any] = None,
274
+ ) -> typing.Iterator[httpx.Response]:
275
+ base_url = self.get_base_url(base_url)
276
+ timeout = (
277
+ request_options.get("timeout_in_seconds")
278
+ if request_options is not None and request_options.get("timeout_in_seconds") is not None
279
+ else self.base_timeout()
280
+ )
281
+
282
+ json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
283
+
284
+ with self.httpx_client.stream(
285
+ method=method,
286
+ url=urllib.parse.urljoin(f"{base_url}/", path),
287
+ headers=jsonable_encoder(
288
+ remove_none_from_dict(
289
+ {
290
+ **self.base_headers(),
291
+ **(headers if headers is not None else {}),
292
+ **(request_options.get("additional_headers", {}) if request_options is not None else {}),
293
+ }
294
+ )
295
+ ),
296
+ params=encode_query(
297
+ jsonable_encoder(
298
+ remove_none_from_dict(
299
+ remove_omit_from_dict(
300
+ {
301
+ **(params if params is not None else {}),
302
+ **(
303
+ request_options.get("additional_query_parameters", {})
304
+ if request_options is not None
305
+ else {}
306
+ ),
307
+ },
308
+ omit,
309
+ )
310
+ )
311
+ )
312
+ ),
313
+ json=json_body,
314
+ data=data_body,
315
+ content=content,
316
+ files=(
317
+ convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
318
+ if (files is not None and files is not omit)
319
+ else None
320
+ ),
321
+ timeout=timeout,
322
+ ) as stream:
323
+ yield stream
324
+
325
+
326
+ class AsyncHttpClient:
327
+ def __init__(
328
+ self,
329
+ *,
330
+ httpx_client: httpx.AsyncClient,
331
+ base_timeout: typing.Callable[[], typing.Optional[float]],
332
+ base_headers: typing.Callable[[], typing.Dict[str, str]],
333
+ base_url: typing.Optional[typing.Callable[[], str]] = None,
334
+ ):
335
+ self.base_url = base_url
336
+ self.base_timeout = base_timeout
337
+ self.base_headers = base_headers
338
+ self.httpx_client = httpx_client
339
+
340
+ def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str:
341
+ base_url = maybe_base_url
342
+ if self.base_url is not None and base_url is None:
343
+ base_url = self.base_url()
344
+
345
+ if base_url is None:
346
+ raise ValueError("A base_url is required to make this request, please provide one and try again.")
347
+ return base_url
348
+
349
+ async def request(
350
+ self,
351
+ path: typing.Optional[str] = None,
352
+ *,
353
+ method: str,
354
+ base_url: typing.Optional[str] = None,
355
+ params: typing.Optional[typing.Dict[str, typing.Any]] = None,
356
+ json: typing.Optional[typing.Any] = None,
357
+ data: typing.Optional[typing.Any] = None,
358
+ content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
359
+ files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None,
360
+ headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
361
+ request_options: typing.Optional[RequestOptions] = None,
362
+ retries: int = 0,
363
+ omit: typing.Optional[typing.Any] = None,
364
+ ) -> httpx.Response:
365
+ base_url = self.get_base_url(base_url)
366
+ timeout = (
367
+ request_options.get("timeout_in_seconds")
368
+ if request_options is not None and request_options.get("timeout_in_seconds") is not None
369
+ else self.base_timeout()
370
+ )
371
+
372
+ json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
373
+
374
+ # Add the input to each of these and do None-safety checks
375
+ response = await self.httpx_client.request(
376
+ method=method,
377
+ url=urllib.parse.urljoin(f"{base_url}/", path),
378
+ headers=jsonable_encoder(
379
+ remove_none_from_dict(
380
+ {
381
+ **self.base_headers(),
382
+ **(headers if headers is not None else {}),
383
+ **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}),
384
+ }
385
+ )
386
+ ),
387
+ params=encode_query(
388
+ jsonable_encoder(
389
+ remove_none_from_dict(
390
+ remove_omit_from_dict(
391
+ {
392
+ **(params if params is not None else {}),
393
+ **(
394
+ request_options.get("additional_query_parameters", {}) or {}
395
+ if request_options is not None
396
+ else {}
397
+ ),
398
+ },
399
+ omit,
400
+ )
401
+ )
402
+ )
403
+ ),
404
+ json=json_body,
405
+ data=data_body,
406
+ content=content,
407
+ files=(
408
+ convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
409
+ if files is not None
410
+ else None
411
+ ),
412
+ timeout=timeout,
413
+ )
414
+
415
+ max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0
416
+ if _should_retry(response=response):
417
+ if max_retries > retries:
418
+ await asyncio.sleep(_retry_timeout(response=response, retries=retries))
419
+ return await self.request(
420
+ path=path,
421
+ method=method,
422
+ base_url=base_url,
423
+ params=params,
424
+ json=json,
425
+ content=content,
426
+ files=files,
427
+ headers=headers,
428
+ request_options=request_options,
429
+ retries=retries + 1,
430
+ omit=omit,
431
+ )
432
+ return response
433
+
434
+ @asynccontextmanager
435
+ async def stream(
436
+ self,
437
+ path: typing.Optional[str] = None,
438
+ *,
439
+ method: str,
440
+ base_url: typing.Optional[str] = None,
441
+ params: typing.Optional[typing.Dict[str, typing.Any]] = None,
442
+ json: typing.Optional[typing.Any] = None,
443
+ data: typing.Optional[typing.Any] = None,
444
+ content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
445
+ files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None,
446
+ headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
447
+ request_options: typing.Optional[RequestOptions] = None,
448
+ retries: int = 0,
449
+ omit: typing.Optional[typing.Any] = None,
450
+ ) -> typing.AsyncIterator[httpx.Response]:
451
+ base_url = self.get_base_url(base_url)
452
+ timeout = (
453
+ request_options.get("timeout_in_seconds")
454
+ if request_options is not None and request_options.get("timeout_in_seconds") is not None
455
+ else self.base_timeout()
456
+ )
457
+
458
+ json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
459
+
460
+ async with self.httpx_client.stream(
461
+ method=method,
462
+ url=urllib.parse.urljoin(f"{base_url}/", path),
463
+ headers=jsonable_encoder(
464
+ remove_none_from_dict(
465
+ {
466
+ **self.base_headers(),
467
+ **(headers if headers is not None else {}),
468
+ **(request_options.get("additional_headers", {}) if request_options is not None else {}),
469
+ }
470
+ )
471
+ ),
472
+ params=encode_query(
473
+ jsonable_encoder(
474
+ remove_none_from_dict(
475
+ remove_omit_from_dict(
476
+ {
477
+ **(params if params is not None else {}),
478
+ **(
479
+ request_options.get("additional_query_parameters", {})
480
+ if request_options is not None
481
+ else {}
482
+ ),
483
+ },
484
+ omit=omit,
485
+ )
486
+ )
487
+ )
488
+ ),
489
+ json=json_body,
490
+ data=data_body,
491
+ content=content,
492
+ files=(
493
+ convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
494
+ if files is not None
495
+ else None
496
+ ),
497
+ timeout=timeout,
498
+ ) as stream:
499
+ yield stream
@@ -0,0 +1,101 @@
1
+ # This file was auto-generated by Fern from our API Definition.
2
+
3
+ """
4
+ jsonable_encoder converts a Python object to a JSON-friendly dict
5
+ (e.g. datetimes to strings, Pydantic models to dicts).
6
+
7
+ Taken from FastAPI, and made a bit simpler
8
+ https://github.com/tiangolo/fastapi/blob/master/fastapi/encoders.py
9
+ """
10
+
11
+ import base64
12
+ import dataclasses
13
+ import datetime as dt
14
+ from enum import Enum
15
+ from pathlib import PurePath
16
+ from types import GeneratorType
17
+ from typing import Any, Callable, Dict, List, Optional, Set, Union
18
+
19
+ import pydantic
20
+
21
+ from .datetime_utils import serialize_datetime
22
+ from .pydantic_utilities import (
23
+ IS_PYDANTIC_V2,
24
+ encode_by_type,
25
+ to_jsonable_with_fallback,
26
+ )
27
+
28
+ SetIntStr = Set[Union[int, str]]
29
+ DictIntStrAny = Dict[Union[int, str], Any]
30
+
31
+
32
+ def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any:
33
+ custom_encoder = custom_encoder or {}
34
+ if custom_encoder:
35
+ if type(obj) in custom_encoder:
36
+ return custom_encoder[type(obj)](obj)
37
+ else:
38
+ for encoder_type, encoder_instance in custom_encoder.items():
39
+ if isinstance(obj, encoder_type):
40
+ return encoder_instance(obj)
41
+ if isinstance(obj, pydantic.BaseModel):
42
+ if IS_PYDANTIC_V2:
43
+ encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2
44
+ else:
45
+ encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1
46
+ if custom_encoder:
47
+ encoder.update(custom_encoder)
48
+ obj_dict = obj.dict(by_alias=True)
49
+ if "__root__" in obj_dict:
50
+ obj_dict = obj_dict["__root__"]
51
+ if "root" in obj_dict:
52
+ obj_dict = obj_dict["root"]
53
+ return jsonable_encoder(obj_dict, custom_encoder=encoder)
54
+ if dataclasses.is_dataclass(obj):
55
+ obj_dict = dataclasses.asdict(obj) # type: ignore
56
+ return jsonable_encoder(obj_dict, custom_encoder=custom_encoder)
57
+ if isinstance(obj, bytes):
58
+ return base64.b64encode(obj).decode("utf-8")
59
+ if isinstance(obj, Enum):
60
+ return obj.value
61
+ if isinstance(obj, PurePath):
62
+ return str(obj)
63
+ if isinstance(obj, (str, int, float, type(None))):
64
+ return obj
65
+ if isinstance(obj, dt.datetime):
66
+ return serialize_datetime(obj)
67
+ if isinstance(obj, dt.date):
68
+ return str(obj)
69
+ if isinstance(obj, dict):
70
+ encoded_dict = {}
71
+ allowed_keys = set(obj.keys())
72
+ for key, value in obj.items():
73
+ if key in allowed_keys:
74
+ encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder)
75
+ encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder)
76
+ encoded_dict[encoded_key] = encoded_value
77
+ return encoded_dict
78
+ if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)):
79
+ encoded_list = []
80
+ for item in obj:
81
+ encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder))
82
+ return encoded_list
83
+
84
+ def fallback_serializer(o: Any) -> Any:
85
+ attempt_encode = encode_by_type(o)
86
+ if attempt_encode is not None:
87
+ return attempt_encode
88
+
89
+ try:
90
+ data = dict(o)
91
+ except Exception as e:
92
+ errors: List[Exception] = []
93
+ errors.append(e)
94
+ try:
95
+ data = vars(o)
96
+ except Exception as e:
97
+ errors.append(e)
98
+ raise ValueError(errors) from e
99
+ return jsonable_encoder(data, custom_encoder=custom_encoder)
100
+
101
+ return to_jsonable_with_fallback(obj, fallback_serializer)
@@ -0,0 +1,88 @@
1
+ # This file was auto-generated by Fern from our API Definition.
2
+
3
+ import typing
4
+
5
+ from typing_extensions import Self
6
+
7
+ import pydantic
8
+
9
+ # Generic to represent the underlying type of the results within a page
10
+ T = typing.TypeVar("T")
11
+
12
+
13
+ # SDKs implement a Page ABC per-pagination request, the endpoint then returns a pager that wraps this type
14
+ # for example, an endpoint will return SyncPager[UserPage] where UserPage implements the Page ABC. ex:
15
+ #
16
+ # SyncPager<InnerListType>(
17
+ # has_next=response.list_metadata.after is not None,
18
+ # items=response.data,
19
+ # # This should be the outer function that returns the SyncPager again
20
+ # get_next=lambda: list(..., cursor: response.cursor) (or list(..., offset: offset + 1))
21
+ # )
22
+ class BasePage(pydantic.BaseModel, typing.Generic[T]):
23
+ has_next: bool
24
+ items: typing.Optional[typing.List[T]]
25
+
26
+
27
+ class SyncPage(BasePage[T], typing.Generic[T]):
28
+ get_next: typing.Optional[typing.Callable[[], typing.Optional[Self]]]
29
+
30
+
31
+ class AsyncPage(BasePage[T], typing.Generic[T]):
32
+ get_next: typing.Optional[typing.Callable[[], typing.Awaitable[typing.Optional[Self]]]]
33
+
34
+
35
+ # ----------------------------
36
+
37
+
38
+ class SyncPager(SyncPage[T], typing.Generic[T]):
39
+ # Here we type ignore the iterator to avoid a mypy error
40
+ # caused by the type conflict with Pydanitc's __iter__ method
41
+ # brought in by extending the base model
42
+ def __iter__(self) -> typing.Iterator[T]: # type: ignore
43
+ for page in self.iter_pages():
44
+ if page.items is not None:
45
+ for item in page.items:
46
+ yield item
47
+
48
+ def iter_pages(self) -> typing.Iterator[SyncPage[T]]:
49
+ page: typing.Union[SyncPager[T], None] = self
50
+ while True:
51
+ if page is not None:
52
+ yield page
53
+ if page.has_next and page.get_next is not None:
54
+ page = page.get_next()
55
+ if page is None or page.items is None or len(page.items) == 0:
56
+ return
57
+ else:
58
+ return
59
+ else:
60
+ return
61
+
62
+ def next_page(self) -> typing.Optional[SyncPage[T]]:
63
+ return self.get_next() if self.get_next is not None else None
64
+
65
+
66
+ class AsyncPager(AsyncPage[T], typing.Generic[T]):
67
+ async def __aiter__(self) -> typing.AsyncIterator[T]: # type: ignore
68
+ async for page in self.iter_pages():
69
+ if page.items is not None:
70
+ for item in page.items:
71
+ yield item
72
+
73
+ async def iter_pages(self) -> typing.AsyncIterator[AsyncPage[T]]:
74
+ page: typing.Union[AsyncPager[T], None] = self
75
+ while True:
76
+ if page is not None:
77
+ yield page
78
+ if page is not None and page.has_next and page.get_next is not None:
79
+ page = await page.get_next()
80
+ if page is None or page.items is None or len(page.items) == 0:
81
+ return
82
+ else:
83
+ return
84
+ else:
85
+ return
86
+
87
+ async def next_page(self) -> typing.Optional[AsyncPage[T]]:
88
+ return await self.get_next() if self.get_next is not None else None