sarvamai 0.1.23a3__py3-none-any.whl → 0.1.23a5__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.
- sarvamai/__init__.py +203 -405
- sarvamai/chat/raw_client.py +20 -20
- sarvamai/client.py +34 -186
- sarvamai/core/__init__.py +21 -76
- sarvamai/core/client_wrapper.py +3 -19
- sarvamai/core/force_multipart.py +2 -4
- sarvamai/core/http_client.py +97 -217
- sarvamai/core/http_response.py +1 -1
- sarvamai/core/jsonable_encoder.py +0 -8
- sarvamai/core/pydantic_utilities.py +4 -110
- sarvamai/errors/__init__.py +6 -40
- sarvamai/errors/bad_request_error.py +1 -1
- sarvamai/errors/forbidden_error.py +1 -1
- sarvamai/errors/internal_server_error.py +1 -1
- sarvamai/errors/service_unavailable_error.py +1 -1
- sarvamai/errors/too_many_requests_error.py +1 -1
- sarvamai/errors/unprocessable_entity_error.py +1 -1
- sarvamai/requests/__init__.py +62 -150
- sarvamai/requests/configure_connection.py +4 -0
- sarvamai/requests/configure_connection_data.py +40 -11
- sarvamai/requests/error_response_data.py +1 -1
- sarvamai/requests/file_signed_url_details.py +1 -1
- sarvamai/requests/speech_to_text_job_parameters.py +43 -2
- sarvamai/requests/speech_to_text_transcription_data.py +2 -2
- sarvamai/requests/speech_to_text_translate_job_parameters.py +4 -1
- sarvamai/speech_to_text/client.py +95 -10
- sarvamai/speech_to_text/raw_client.py +147 -64
- sarvamai/speech_to_text_job/client.py +60 -15
- sarvamai/speech_to_text_job/raw_client.py +120 -120
- sarvamai/speech_to_text_streaming/__init__.py +10 -38
- sarvamai/speech_to_text_streaming/client.py +90 -8
- sarvamai/speech_to_text_streaming/raw_client.py +90 -8
- sarvamai/speech_to_text_streaming/types/__init__.py +8 -36
- sarvamai/speech_to_text_streaming/types/speech_to_text_streaming_mode.py +7 -0
- sarvamai/speech_to_text_streaming/types/speech_to_text_streaming_model.py +5 -0
- sarvamai/speech_to_text_translate_job/raw_client.py +120 -120
- sarvamai/speech_to_text_translate_streaming/__init__.py +5 -36
- sarvamai/speech_to_text_translate_streaming/client.py +8 -2
- sarvamai/speech_to_text_translate_streaming/raw_client.py +8 -2
- sarvamai/speech_to_text_translate_streaming/types/__init__.py +3 -36
- sarvamai/text/raw_client.py +60 -60
- sarvamai/text_to_speech/client.py +100 -16
- sarvamai/text_to_speech/raw_client.py +120 -36
- sarvamai/text_to_speech_streaming/__init__.py +2 -29
- sarvamai/text_to_speech_streaming/client.py +19 -6
- sarvamai/text_to_speech_streaming/raw_client.py +19 -6
- sarvamai/text_to_speech_streaming/types/__init__.py +3 -31
- sarvamai/text_to_speech_streaming/types/text_to_speech_streaming_model.py +5 -0
- sarvamai/types/__init__.py +102 -222
- sarvamai/types/chat_completion_request_message.py +2 -6
- sarvamai/types/configure_connection.py +4 -0
- sarvamai/types/configure_connection_data.py +40 -11
- sarvamai/types/configure_connection_data_model.py +5 -0
- sarvamai/types/configure_connection_data_speaker.py +35 -1
- sarvamai/types/error_response_data.py +1 -1
- sarvamai/types/file_signed_url_details.py +1 -1
- sarvamai/types/mode.py +5 -0
- sarvamai/types/speech_to_text_job_parameters.py +43 -2
- sarvamai/types/speech_to_text_model.py +1 -1
- sarvamai/types/speech_to_text_transcription_data.py +2 -2
- sarvamai/types/speech_to_text_translate_job_parameters.py +4 -1
- sarvamai/types/text_to_speech_model.py +1 -1
- sarvamai/types/text_to_speech_speaker.py +35 -1
- {sarvamai-0.1.23a3.dist-info → sarvamai-0.1.23a5.dist-info}/METADATA +1 -2
- {sarvamai-0.1.23a3.dist-info → sarvamai-0.1.23a5.dist-info}/RECORD +66 -66
- sarvamai/core/http_sse/__init__.py +0 -42
- sarvamai/core/http_sse/_api.py +0 -112
- sarvamai/core/http_sse/_decoders.py +0 -61
- sarvamai/core/http_sse/_exceptions.py +0 -7
- sarvamai/core/http_sse/_models.py +0 -17
- {sarvamai-0.1.23a3.dist-info → sarvamai-0.1.23a5.dist-info}/WHEEL +0 -0
sarvamai/core/http_client.py
CHANGED
|
@@ -5,6 +5,7 @@ import email.utils
|
|
|
5
5
|
import re
|
|
6
6
|
import time
|
|
7
7
|
import typing
|
|
8
|
+
import urllib.parse
|
|
8
9
|
from contextlib import asynccontextmanager, contextmanager
|
|
9
10
|
from random import random
|
|
10
11
|
|
|
@@ -13,13 +14,13 @@ from .file import File, convert_file_dict_to_httpx_tuples
|
|
|
13
14
|
from .force_multipart import FORCE_MULTIPART
|
|
14
15
|
from .jsonable_encoder import jsonable_encoder
|
|
15
16
|
from .query_encoder import encode_query
|
|
16
|
-
from .remove_none_from_dict import remove_none_from_dict
|
|
17
|
+
from .remove_none_from_dict import remove_none_from_dict
|
|
17
18
|
from .request_options import RequestOptions
|
|
18
19
|
from httpx._types import RequestFiles
|
|
19
20
|
|
|
20
|
-
INITIAL_RETRY_DELAY_SECONDS =
|
|
21
|
-
MAX_RETRY_DELAY_SECONDS =
|
|
22
|
-
|
|
21
|
+
INITIAL_RETRY_DELAY_SECONDS = 0.5
|
|
22
|
+
MAX_RETRY_DELAY_SECONDS = 10
|
|
23
|
+
MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]:
|
|
@@ -63,38 +64,6 @@ def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float
|
|
|
63
64
|
return seconds
|
|
64
65
|
|
|
65
66
|
|
|
66
|
-
def _add_positive_jitter(delay: float) -> float:
|
|
67
|
-
"""Add positive jitter (0-20%) to prevent thundering herd."""
|
|
68
|
-
jitter_multiplier = 1 + random() * JITTER_FACTOR
|
|
69
|
-
return delay * jitter_multiplier
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _add_symmetric_jitter(delay: float) -> float:
|
|
73
|
-
"""Add symmetric jitter (±10%) for exponential backoff."""
|
|
74
|
-
jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR
|
|
75
|
-
return delay * jitter_multiplier
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]:
|
|
79
|
-
"""
|
|
80
|
-
Parse the X-RateLimit-Reset header (Unix timestamp in seconds).
|
|
81
|
-
Returns seconds to wait, or None if header is missing/invalid.
|
|
82
|
-
"""
|
|
83
|
-
reset_time_str = response_headers.get("x-ratelimit-reset")
|
|
84
|
-
if reset_time_str is None:
|
|
85
|
-
return None
|
|
86
|
-
|
|
87
|
-
try:
|
|
88
|
-
reset_time = int(reset_time_str)
|
|
89
|
-
delay = reset_time - time.time()
|
|
90
|
-
if delay > 0:
|
|
91
|
-
return delay
|
|
92
|
-
except (ValueError, TypeError):
|
|
93
|
-
pass
|
|
94
|
-
|
|
95
|
-
return None
|
|
96
|
-
|
|
97
|
-
|
|
98
67
|
def _retry_timeout(response: httpx.Response, retries: int) -> float:
|
|
99
68
|
"""
|
|
100
69
|
Determine the amount of time to wait before retrying a request.
|
|
@@ -102,19 +71,17 @@ def _retry_timeout(response: httpx.Response, retries: int) -> float:
|
|
|
102
71
|
with a jitter to determine the number of seconds to wait.
|
|
103
72
|
"""
|
|
104
73
|
|
|
105
|
-
#
|
|
74
|
+
# If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says.
|
|
106
75
|
retry_after = _parse_retry_after(response.headers)
|
|
107
|
-
if retry_after is not None and retry_after
|
|
108
|
-
return
|
|
76
|
+
if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER:
|
|
77
|
+
return retry_after
|
|
109
78
|
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
if ratelimit_reset is not None:
|
|
113
|
-
return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS))
|
|
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)
|
|
114
81
|
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
return
|
|
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
|
|
118
85
|
|
|
119
86
|
|
|
120
87
|
def _should_retry(response: httpx.Response) -> bool:
|
|
@@ -122,45 +89,6 @@ def _should_retry(response: httpx.Response) -> bool:
|
|
|
122
89
|
return response.status_code >= 500 or response.status_code in retryable_400s
|
|
123
90
|
|
|
124
91
|
|
|
125
|
-
def _build_url(base_url: str, path: typing.Optional[str]) -> str:
|
|
126
|
-
"""
|
|
127
|
-
Build a full URL by joining a base URL with a path.
|
|
128
|
-
|
|
129
|
-
This function correctly handles base URLs that contain path prefixes (e.g., tenant-based URLs)
|
|
130
|
-
by using string concatenation instead of urllib.parse.urljoin(), which would incorrectly
|
|
131
|
-
strip path components when the path starts with '/'.
|
|
132
|
-
|
|
133
|
-
Example:
|
|
134
|
-
>>> _build_url("https://cloud.example.com/org/tenant/api", "/users")
|
|
135
|
-
'https://cloud.example.com/org/tenant/api/users'
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
base_url: The base URL, which may contain path prefixes.
|
|
139
|
-
path: The path to append. Can be None or empty string.
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
The full URL with base_url and path properly joined.
|
|
143
|
-
"""
|
|
144
|
-
if not path:
|
|
145
|
-
return base_url
|
|
146
|
-
return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def _maybe_filter_none_from_multipart_data(
|
|
150
|
-
data: typing.Optional[typing.Any],
|
|
151
|
-
request_files: typing.Optional[RequestFiles],
|
|
152
|
-
force_multipart: typing.Optional[bool],
|
|
153
|
-
) -> typing.Optional[typing.Any]:
|
|
154
|
-
"""
|
|
155
|
-
Filter None values from data body for multipart/form requests.
|
|
156
|
-
This prevents httpx from converting None to empty strings in multipart encoding.
|
|
157
|
-
Only applies when files are present or force_multipart is True.
|
|
158
|
-
"""
|
|
159
|
-
if data is not None and isinstance(data, typing.Mapping) and (request_files or force_multipart):
|
|
160
|
-
return remove_none_from_dict(data)
|
|
161
|
-
return data
|
|
162
|
-
|
|
163
|
-
|
|
164
92
|
def remove_omit_from_dict(
|
|
165
93
|
original: typing.Dict[str, typing.Optional[typing.Any]],
|
|
166
94
|
omit: typing.Optional[typing.Any],
|
|
@@ -215,19 +143,8 @@ def get_request_body(
|
|
|
215
143
|
# If both data and json are None, we send json data in the event extra properties are specified
|
|
216
144
|
json_body = maybe_filter_request_body(json, request_options, omit)
|
|
217
145
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
# Only collapse empty dict to None when the body was not explicitly provided
|
|
223
|
-
# and there are no additional body parameters. This preserves explicit empty
|
|
224
|
-
# bodies (e.g., when an endpoint has a request body type but all fields are optional).
|
|
225
|
-
if json_body == {} and json is None and not has_additional_body_parameters:
|
|
226
|
-
json_body = None
|
|
227
|
-
if data_body == {} and data is None and not has_additional_body_parameters:
|
|
228
|
-
data_body = None
|
|
229
|
-
|
|
230
|
-
return json_body, data_body
|
|
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
|
|
231
148
|
|
|
232
149
|
|
|
233
150
|
class HttpClient:
|
|
@@ -271,7 +188,7 @@ class HttpClient:
|
|
|
271
188
|
] = None,
|
|
272
189
|
headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
273
190
|
request_options: typing.Optional[RequestOptions] = None,
|
|
274
|
-
retries: int =
|
|
191
|
+
retries: int = 2,
|
|
275
192
|
omit: typing.Optional[typing.Any] = None,
|
|
276
193
|
force_multipart: typing.Optional[bool] = None,
|
|
277
194
|
) -> httpx.Response:
|
|
@@ -293,31 +210,9 @@ class HttpClient:
|
|
|
293
210
|
if (request_files is None or len(request_files) == 0) and force_multipart:
|
|
294
211
|
request_files = FORCE_MULTIPART
|
|
295
212
|
|
|
296
|
-
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
|
|
297
|
-
|
|
298
|
-
# Compute encoded params separately to avoid passing empty list to httpx
|
|
299
|
-
# (httpx strips existing query params from URL when params=[] is passed)
|
|
300
|
-
_encoded_params = encode_query(
|
|
301
|
-
jsonable_encoder(
|
|
302
|
-
remove_none_from_dict(
|
|
303
|
-
remove_omit_from_dict(
|
|
304
|
-
{
|
|
305
|
-
**(params if params is not None else {}),
|
|
306
|
-
**(
|
|
307
|
-
request_options.get("additional_query_parameters", {}) or {}
|
|
308
|
-
if request_options is not None
|
|
309
|
-
else {}
|
|
310
|
-
),
|
|
311
|
-
},
|
|
312
|
-
omit,
|
|
313
|
-
)
|
|
314
|
-
)
|
|
315
|
-
)
|
|
316
|
-
)
|
|
317
|
-
|
|
318
213
|
response = self.httpx_client.request(
|
|
319
214
|
method=method,
|
|
320
|
-
url=
|
|
215
|
+
url=urllib.parse.urljoin(f"{base_url}/", path),
|
|
321
216
|
headers=jsonable_encoder(
|
|
322
217
|
remove_none_from_dict(
|
|
323
218
|
{
|
|
@@ -327,7 +222,23 @@ class HttpClient:
|
|
|
327
222
|
}
|
|
328
223
|
)
|
|
329
224
|
),
|
|
330
|
-
params=
|
|
225
|
+
params=encode_query(
|
|
226
|
+
jsonable_encoder(
|
|
227
|
+
remove_none_from_dict(
|
|
228
|
+
remove_omit_from_dict(
|
|
229
|
+
{
|
|
230
|
+
**(params if params is not None else {}),
|
|
231
|
+
**(
|
|
232
|
+
request_options.get("additional_query_parameters", {}) or {}
|
|
233
|
+
if request_options is not None
|
|
234
|
+
else {}
|
|
235
|
+
),
|
|
236
|
+
},
|
|
237
|
+
omit,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
),
|
|
331
242
|
json=json_body,
|
|
332
243
|
data=data_body,
|
|
333
244
|
content=content,
|
|
@@ -335,9 +246,9 @@ class HttpClient:
|
|
|
335
246
|
timeout=timeout,
|
|
336
247
|
)
|
|
337
248
|
|
|
338
|
-
max_retries: int = request_options.get("max_retries",
|
|
249
|
+
max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0
|
|
339
250
|
if _should_retry(response=response):
|
|
340
|
-
if
|
|
251
|
+
if max_retries > retries:
|
|
341
252
|
time.sleep(_retry_timeout(response=response, retries=retries))
|
|
342
253
|
return self.request(
|
|
343
254
|
path=path,
|
|
@@ -374,7 +285,7 @@ class HttpClient:
|
|
|
374
285
|
] = None,
|
|
375
286
|
headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
376
287
|
request_options: typing.Optional[RequestOptions] = None,
|
|
377
|
-
retries: int =
|
|
288
|
+
retries: int = 2,
|
|
378
289
|
omit: typing.Optional[typing.Any] = None,
|
|
379
290
|
force_multipart: typing.Optional[bool] = None,
|
|
380
291
|
) -> typing.Iterator[httpx.Response]:
|
|
@@ -396,31 +307,9 @@ class HttpClient:
|
|
|
396
307
|
|
|
397
308
|
json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
|
|
398
309
|
|
|
399
|
-
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
|
|
400
|
-
|
|
401
|
-
# Compute encoded params separately to avoid passing empty list to httpx
|
|
402
|
-
# (httpx strips existing query params from URL when params=[] is passed)
|
|
403
|
-
_encoded_params = encode_query(
|
|
404
|
-
jsonable_encoder(
|
|
405
|
-
remove_none_from_dict(
|
|
406
|
-
remove_omit_from_dict(
|
|
407
|
-
{
|
|
408
|
-
**(params if params is not None else {}),
|
|
409
|
-
**(
|
|
410
|
-
request_options.get("additional_query_parameters", {})
|
|
411
|
-
if request_options is not None
|
|
412
|
-
else {}
|
|
413
|
-
),
|
|
414
|
-
},
|
|
415
|
-
omit,
|
|
416
|
-
)
|
|
417
|
-
)
|
|
418
|
-
)
|
|
419
|
-
)
|
|
420
|
-
|
|
421
310
|
with self.httpx_client.stream(
|
|
422
311
|
method=method,
|
|
423
|
-
url=
|
|
312
|
+
url=urllib.parse.urljoin(f"{base_url}/", path),
|
|
424
313
|
headers=jsonable_encoder(
|
|
425
314
|
remove_none_from_dict(
|
|
426
315
|
{
|
|
@@ -430,7 +319,23 @@ class HttpClient:
|
|
|
430
319
|
}
|
|
431
320
|
)
|
|
432
321
|
),
|
|
433
|
-
params=
|
|
322
|
+
params=encode_query(
|
|
323
|
+
jsonable_encoder(
|
|
324
|
+
remove_none_from_dict(
|
|
325
|
+
remove_omit_from_dict(
|
|
326
|
+
{
|
|
327
|
+
**(params if params is not None else {}),
|
|
328
|
+
**(
|
|
329
|
+
request_options.get("additional_query_parameters", {})
|
|
330
|
+
if request_options is not None
|
|
331
|
+
else {}
|
|
332
|
+
),
|
|
333
|
+
},
|
|
334
|
+
omit,
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
),
|
|
434
339
|
json=json_body,
|
|
435
340
|
data=data_body,
|
|
436
341
|
content=content,
|
|
@@ -448,19 +353,12 @@ class AsyncHttpClient:
|
|
|
448
353
|
base_timeout: typing.Callable[[], typing.Optional[float]],
|
|
449
354
|
base_headers: typing.Callable[[], typing.Dict[str, str]],
|
|
450
355
|
base_url: typing.Optional[typing.Callable[[], str]] = None,
|
|
451
|
-
async_base_headers: typing.Optional[typing.Callable[[], typing.Awaitable[typing.Dict[str, str]]]] = None,
|
|
452
356
|
):
|
|
453
357
|
self.base_url = base_url
|
|
454
358
|
self.base_timeout = base_timeout
|
|
455
359
|
self.base_headers = base_headers
|
|
456
|
-
self.async_base_headers = async_base_headers
|
|
457
360
|
self.httpx_client = httpx_client
|
|
458
361
|
|
|
459
|
-
async def _get_headers(self) -> typing.Dict[str, str]:
|
|
460
|
-
if self.async_base_headers is not None:
|
|
461
|
-
return await self.async_base_headers()
|
|
462
|
-
return self.base_headers()
|
|
463
|
-
|
|
464
362
|
def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str:
|
|
465
363
|
base_url = maybe_base_url
|
|
466
364
|
if self.base_url is not None and base_url is None:
|
|
@@ -488,7 +386,7 @@ class AsyncHttpClient:
|
|
|
488
386
|
] = None,
|
|
489
387
|
headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
490
388
|
request_options: typing.Optional[RequestOptions] = None,
|
|
491
|
-
retries: int =
|
|
389
|
+
retries: int = 2,
|
|
492
390
|
omit: typing.Optional[typing.Any] = None,
|
|
493
391
|
force_multipart: typing.Optional[bool] = None,
|
|
494
392
|
) -> httpx.Response:
|
|
@@ -510,45 +408,36 @@ class AsyncHttpClient:
|
|
|
510
408
|
|
|
511
409
|
json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
|
|
512
410
|
|
|
513
|
-
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
|
|
514
|
-
|
|
515
|
-
# Get headers (supports async token providers)
|
|
516
|
-
_headers = await self._get_headers()
|
|
517
|
-
|
|
518
|
-
# Compute encoded params separately to avoid passing empty list to httpx
|
|
519
|
-
# (httpx strips existing query params from URL when params=[] is passed)
|
|
520
|
-
_encoded_params = encode_query(
|
|
521
|
-
jsonable_encoder(
|
|
522
|
-
remove_none_from_dict(
|
|
523
|
-
remove_omit_from_dict(
|
|
524
|
-
{
|
|
525
|
-
**(params if params is not None else {}),
|
|
526
|
-
**(
|
|
527
|
-
request_options.get("additional_query_parameters", {}) or {}
|
|
528
|
-
if request_options is not None
|
|
529
|
-
else {}
|
|
530
|
-
),
|
|
531
|
-
},
|
|
532
|
-
omit,
|
|
533
|
-
)
|
|
534
|
-
)
|
|
535
|
-
)
|
|
536
|
-
)
|
|
537
|
-
|
|
538
411
|
# Add the input to each of these and do None-safety checks
|
|
539
412
|
response = await self.httpx_client.request(
|
|
540
413
|
method=method,
|
|
541
|
-
url=
|
|
414
|
+
url=urllib.parse.urljoin(f"{base_url}/", path),
|
|
542
415
|
headers=jsonable_encoder(
|
|
543
416
|
remove_none_from_dict(
|
|
544
417
|
{
|
|
545
|
-
**
|
|
418
|
+
**self.base_headers(),
|
|
546
419
|
**(headers if headers is not None else {}),
|
|
547
420
|
**(request_options.get("additional_headers", {}) or {} if request_options is not None else {}),
|
|
548
421
|
}
|
|
549
422
|
)
|
|
550
423
|
),
|
|
551
|
-
params=
|
|
424
|
+
params=encode_query(
|
|
425
|
+
jsonable_encoder(
|
|
426
|
+
remove_none_from_dict(
|
|
427
|
+
remove_omit_from_dict(
|
|
428
|
+
{
|
|
429
|
+
**(params if params is not None else {}),
|
|
430
|
+
**(
|
|
431
|
+
request_options.get("additional_query_parameters", {}) or {}
|
|
432
|
+
if request_options is not None
|
|
433
|
+
else {}
|
|
434
|
+
),
|
|
435
|
+
},
|
|
436
|
+
omit,
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
),
|
|
552
441
|
json=json_body,
|
|
553
442
|
data=data_body,
|
|
554
443
|
content=content,
|
|
@@ -556,9 +445,9 @@ class AsyncHttpClient:
|
|
|
556
445
|
timeout=timeout,
|
|
557
446
|
)
|
|
558
447
|
|
|
559
|
-
max_retries: int = request_options.get("max_retries",
|
|
448
|
+
max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0
|
|
560
449
|
if _should_retry(response=response):
|
|
561
|
-
if
|
|
450
|
+
if max_retries > retries:
|
|
562
451
|
await asyncio.sleep(_retry_timeout(response=response, retries=retries))
|
|
563
452
|
return await self.request(
|
|
564
453
|
path=path,
|
|
@@ -594,7 +483,7 @@ class AsyncHttpClient:
|
|
|
594
483
|
] = None,
|
|
595
484
|
headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
596
485
|
request_options: typing.Optional[RequestOptions] = None,
|
|
597
|
-
retries: int =
|
|
486
|
+
retries: int = 2,
|
|
598
487
|
omit: typing.Optional[typing.Any] = None,
|
|
599
488
|
force_multipart: typing.Optional[bool] = None,
|
|
600
489
|
) -> typing.AsyncIterator[httpx.Response]:
|
|
@@ -616,44 +505,35 @@ class AsyncHttpClient:
|
|
|
616
505
|
|
|
617
506
|
json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
|
|
618
507
|
|
|
619
|
-
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
|
|
620
|
-
|
|
621
|
-
# Get headers (supports async token providers)
|
|
622
|
-
_headers = await self._get_headers()
|
|
623
|
-
|
|
624
|
-
# Compute encoded params separately to avoid passing empty list to httpx
|
|
625
|
-
# (httpx strips existing query params from URL when params=[] is passed)
|
|
626
|
-
_encoded_params = encode_query(
|
|
627
|
-
jsonable_encoder(
|
|
628
|
-
remove_none_from_dict(
|
|
629
|
-
remove_omit_from_dict(
|
|
630
|
-
{
|
|
631
|
-
**(params if params is not None else {}),
|
|
632
|
-
**(
|
|
633
|
-
request_options.get("additional_query_parameters", {})
|
|
634
|
-
if request_options is not None
|
|
635
|
-
else {}
|
|
636
|
-
),
|
|
637
|
-
},
|
|
638
|
-
omit=omit,
|
|
639
|
-
)
|
|
640
|
-
)
|
|
641
|
-
)
|
|
642
|
-
)
|
|
643
|
-
|
|
644
508
|
async with self.httpx_client.stream(
|
|
645
509
|
method=method,
|
|
646
|
-
url=
|
|
510
|
+
url=urllib.parse.urljoin(f"{base_url}/", path),
|
|
647
511
|
headers=jsonable_encoder(
|
|
648
512
|
remove_none_from_dict(
|
|
649
513
|
{
|
|
650
|
-
**
|
|
514
|
+
**self.base_headers(),
|
|
651
515
|
**(headers if headers is not None else {}),
|
|
652
516
|
**(request_options.get("additional_headers", {}) if request_options is not None else {}),
|
|
653
517
|
}
|
|
654
518
|
)
|
|
655
519
|
),
|
|
656
|
-
params=
|
|
520
|
+
params=encode_query(
|
|
521
|
+
jsonable_encoder(
|
|
522
|
+
remove_none_from_dict(
|
|
523
|
+
remove_omit_from_dict(
|
|
524
|
+
{
|
|
525
|
+
**(params if params is not None else {}),
|
|
526
|
+
**(
|
|
527
|
+
request_options.get("additional_query_parameters", {})
|
|
528
|
+
if request_options is not None
|
|
529
|
+
else {}
|
|
530
|
+
),
|
|
531
|
+
},
|
|
532
|
+
omit=omit,
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
)
|
|
536
|
+
),
|
|
657
537
|
json=json_body,
|
|
658
538
|
data=data_body,
|
|
659
539
|
content=content,
|
sarvamai/core/http_response.py
CHANGED
|
@@ -4,8 +4,8 @@ from typing import Dict, Generic, TypeVar
|
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
|
|
7
|
-
# Generic to represent the underlying type of the data wrapped by the HTTP response.
|
|
8
7
|
T = TypeVar("T")
|
|
8
|
+
"""Generic to represent the underlying type of the data wrapped by the HTTP response."""
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class BaseHttpResponse:
|
|
@@ -30,10 +30,6 @@ DictIntStrAny = Dict[Union[int, str], Any]
|
|
|
30
30
|
|
|
31
31
|
def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any:
|
|
32
32
|
custom_encoder = custom_encoder or {}
|
|
33
|
-
# Generated SDKs use Ellipsis (`...`) as the sentinel value for "OMIT".
|
|
34
|
-
# OMIT values should be excluded from serialized payloads.
|
|
35
|
-
if obj is Ellipsis:
|
|
36
|
-
return None
|
|
37
33
|
if custom_encoder:
|
|
38
34
|
if type(obj) in custom_encoder:
|
|
39
35
|
return custom_encoder[type(obj)](obj)
|
|
@@ -74,8 +70,6 @@ def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any]
|
|
|
74
70
|
allowed_keys = set(obj.keys())
|
|
75
71
|
for key, value in obj.items():
|
|
76
72
|
if key in allowed_keys:
|
|
77
|
-
if value is Ellipsis:
|
|
78
|
-
continue
|
|
79
73
|
encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder)
|
|
80
74
|
encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder)
|
|
81
75
|
encoded_dict[encoded_key] = encoded_value
|
|
@@ -83,8 +77,6 @@ def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any]
|
|
|
83
77
|
if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)):
|
|
84
78
|
encoded_list = []
|
|
85
79
|
for item in obj:
|
|
86
|
-
if item is Ellipsis:
|
|
87
|
-
continue
|
|
88
80
|
encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder))
|
|
89
81
|
return encoded_list
|
|
90
82
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
# nopycln: file
|
|
4
4
|
import datetime as dt
|
|
5
|
-
import inspect
|
|
6
5
|
from collections import defaultdict
|
|
7
6
|
from typing import Any, Callable, ClassVar, Dict, List, Mapping, Optional, Set, Tuple, Type, TypeVar, Union, cast
|
|
8
7
|
|
|
@@ -38,36 +37,7 @@ Model = TypeVar("Model", bound=pydantic.BaseModel)
|
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
def parse_obj_as(type_: Type[T], object_: Any) -> T:
|
|
41
|
-
|
|
42
|
-
#
|
|
43
|
-
# For Pydantic models, whether we should pre-dealias depends on how the model encodes aliasing:
|
|
44
|
-
# - If the model uses real Pydantic aliases (pydantic.Field(alias=...)), then we must pass wire keys through
|
|
45
|
-
# unchanged so Pydantic can validate them.
|
|
46
|
-
# - If the model encodes aliasing only via FieldMetadata annotations, then we MUST pre-dealias because Pydantic
|
|
47
|
-
# will not recognize those aliases during validation.
|
|
48
|
-
if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel):
|
|
49
|
-
has_pydantic_aliases = False
|
|
50
|
-
if IS_PYDANTIC_V2:
|
|
51
|
-
for field_name, field_info in getattr(type_, "model_fields", {}).items(): # type: ignore[attr-defined]
|
|
52
|
-
alias = getattr(field_info, "alias", None)
|
|
53
|
-
if alias is not None and alias != field_name:
|
|
54
|
-
has_pydantic_aliases = True
|
|
55
|
-
break
|
|
56
|
-
else:
|
|
57
|
-
for field in getattr(type_, "__fields__", {}).values():
|
|
58
|
-
alias = getattr(field, "alias", None)
|
|
59
|
-
name = getattr(field, "name", None)
|
|
60
|
-
if alias is not None and name is not None and alias != name:
|
|
61
|
-
has_pydantic_aliases = True
|
|
62
|
-
break
|
|
63
|
-
|
|
64
|
-
dealiased_object = (
|
|
65
|
-
object_
|
|
66
|
-
if has_pydantic_aliases
|
|
67
|
-
else convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read")
|
|
68
|
-
)
|
|
69
|
-
else:
|
|
70
|
-
dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read")
|
|
40
|
+
dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read")
|
|
71
41
|
if IS_PYDANTIC_V2:
|
|
72
42
|
adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined]
|
|
73
43
|
return adapter.validate_python(dealiased_object)
|
|
@@ -89,46 +59,9 @@ class UniversalBaseModel(pydantic.BaseModel):
|
|
|
89
59
|
protected_namespaces=(),
|
|
90
60
|
)
|
|
91
61
|
|
|
92
|
-
@pydantic.model_validator(mode="before") # type: ignore[attr-defined]
|
|
93
|
-
@classmethod
|
|
94
|
-
def _coerce_field_names_to_aliases(cls, data: Any) -> Any:
|
|
95
|
-
"""
|
|
96
|
-
Accept Python field names in input by rewriting them to their Pydantic aliases,
|
|
97
|
-
while avoiding silent collisions when a key could refer to multiple fields.
|
|
98
|
-
"""
|
|
99
|
-
if not isinstance(data, Mapping):
|
|
100
|
-
return data
|
|
101
|
-
|
|
102
|
-
fields = getattr(cls, "model_fields", {}) # type: ignore[attr-defined]
|
|
103
|
-
name_to_alias: Dict[str, str] = {}
|
|
104
|
-
alias_to_name: Dict[str, str] = {}
|
|
105
|
-
|
|
106
|
-
for name, field_info in fields.items():
|
|
107
|
-
alias = getattr(field_info, "alias", None) or name
|
|
108
|
-
name_to_alias[name] = alias
|
|
109
|
-
if alias != name:
|
|
110
|
-
alias_to_name[alias] = name
|
|
111
|
-
|
|
112
|
-
# Detect ambiguous keys: a key that is an alias for one field and a name for another.
|
|
113
|
-
ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys()))
|
|
114
|
-
for key in ambiguous_keys:
|
|
115
|
-
if key in data and name_to_alias[key] not in data:
|
|
116
|
-
raise ValueError(
|
|
117
|
-
f"Ambiguous input key '{key}': it is both a field name and an alias. "
|
|
118
|
-
"Provide the explicit alias key to disambiguate."
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
original_keys = set(data.keys())
|
|
122
|
-
rewritten: Dict[str, Any] = dict(data)
|
|
123
|
-
for name, alias in name_to_alias.items():
|
|
124
|
-
if alias != name and name in original_keys and alias not in rewritten:
|
|
125
|
-
rewritten[alias] = rewritten.pop(name)
|
|
126
|
-
|
|
127
|
-
return rewritten
|
|
128
|
-
|
|
129
62
|
@pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined]
|
|
130
63
|
def serialize_model(self) -> Any: # type: ignore[name-defined]
|
|
131
|
-
serialized = self.
|
|
64
|
+
serialized = self.model_dump()
|
|
132
65
|
data = {k: serialize_datetime(v) if isinstance(v, dt.datetime) else v for k, v in serialized.items()}
|
|
133
66
|
return data
|
|
134
67
|
|
|
@@ -138,40 +71,6 @@ class UniversalBaseModel(pydantic.BaseModel):
|
|
|
138
71
|
smart_union = True
|
|
139
72
|
json_encoders = {dt.datetime: serialize_datetime}
|
|
140
73
|
|
|
141
|
-
@pydantic.root_validator(pre=True)
|
|
142
|
-
def _coerce_field_names_to_aliases(cls, values: Any) -> Any:
|
|
143
|
-
"""
|
|
144
|
-
Pydantic v1 equivalent of _coerce_field_names_to_aliases.
|
|
145
|
-
"""
|
|
146
|
-
if not isinstance(values, Mapping):
|
|
147
|
-
return values
|
|
148
|
-
|
|
149
|
-
fields = getattr(cls, "__fields__", {})
|
|
150
|
-
name_to_alias: Dict[str, str] = {}
|
|
151
|
-
alias_to_name: Dict[str, str] = {}
|
|
152
|
-
|
|
153
|
-
for name, field in fields.items():
|
|
154
|
-
alias = getattr(field, "alias", None) or name
|
|
155
|
-
name_to_alias[name] = alias
|
|
156
|
-
if alias != name:
|
|
157
|
-
alias_to_name[alias] = name
|
|
158
|
-
|
|
159
|
-
ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys()))
|
|
160
|
-
for key in ambiguous_keys:
|
|
161
|
-
if key in values and name_to_alias[key] not in values:
|
|
162
|
-
raise ValueError(
|
|
163
|
-
f"Ambiguous input key '{key}': it is both a field name and an alias. "
|
|
164
|
-
"Provide the explicit alias key to disambiguate."
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
original_keys = set(values.keys())
|
|
168
|
-
rewritten: Dict[str, Any] = dict(values)
|
|
169
|
-
for name, alias in name_to_alias.items():
|
|
170
|
-
if alias != name and name in original_keys and alias not in rewritten:
|
|
171
|
-
rewritten[alias] = rewritten.pop(name)
|
|
172
|
-
|
|
173
|
-
return rewritten
|
|
174
|
-
|
|
175
74
|
@classmethod
|
|
176
75
|
def model_construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model":
|
|
177
76
|
dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read")
|
|
@@ -248,10 +147,7 @@ class UniversalBaseModel(pydantic.BaseModel):
|
|
|
248
147
|
|
|
249
148
|
dict_dump = super().dict(**kwargs_with_defaults_exclude_unset_include_fields)
|
|
250
149
|
|
|
251
|
-
return
|
|
252
|
-
Dict[str, Any],
|
|
253
|
-
convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write"),
|
|
254
|
-
)
|
|
150
|
+
return convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write")
|
|
255
151
|
|
|
256
152
|
|
|
257
153
|
def _union_list_of_pydantic_dicts(source: List[Any], destination: List[Any]) -> List[Any]:
|
|
@@ -321,9 +217,7 @@ def universal_root_validator(
|
|
|
321
217
|
) -> Callable[[AnyCallable], AnyCallable]:
|
|
322
218
|
def decorator(func: AnyCallable) -> AnyCallable:
|
|
323
219
|
if IS_PYDANTIC_V2:
|
|
324
|
-
|
|
325
|
-
# The custom validators transform the input value before the model is created
|
|
326
|
-
return cast(AnyCallable, pydantic.model_validator(mode="before")(func)) # type: ignore[attr-defined]
|
|
220
|
+
return cast(AnyCallable, pydantic.model_validator(mode="before" if pre else "after")(func)) # type: ignore[attr-defined]
|
|
327
221
|
return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload]
|
|
328
222
|
|
|
329
223
|
return decorator
|