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.
Files changed (71) hide show
  1. sarvamai/__init__.py +203 -405
  2. sarvamai/chat/raw_client.py +20 -20
  3. sarvamai/client.py +34 -186
  4. sarvamai/core/__init__.py +21 -76
  5. sarvamai/core/client_wrapper.py +3 -19
  6. sarvamai/core/force_multipart.py +2 -4
  7. sarvamai/core/http_client.py +97 -217
  8. sarvamai/core/http_response.py +1 -1
  9. sarvamai/core/jsonable_encoder.py +0 -8
  10. sarvamai/core/pydantic_utilities.py +4 -110
  11. sarvamai/errors/__init__.py +6 -40
  12. sarvamai/errors/bad_request_error.py +1 -1
  13. sarvamai/errors/forbidden_error.py +1 -1
  14. sarvamai/errors/internal_server_error.py +1 -1
  15. sarvamai/errors/service_unavailable_error.py +1 -1
  16. sarvamai/errors/too_many_requests_error.py +1 -1
  17. sarvamai/errors/unprocessable_entity_error.py +1 -1
  18. sarvamai/requests/__init__.py +62 -150
  19. sarvamai/requests/configure_connection.py +4 -0
  20. sarvamai/requests/configure_connection_data.py +40 -11
  21. sarvamai/requests/error_response_data.py +1 -1
  22. sarvamai/requests/file_signed_url_details.py +1 -1
  23. sarvamai/requests/speech_to_text_job_parameters.py +43 -2
  24. sarvamai/requests/speech_to_text_transcription_data.py +2 -2
  25. sarvamai/requests/speech_to_text_translate_job_parameters.py +4 -1
  26. sarvamai/speech_to_text/client.py +95 -10
  27. sarvamai/speech_to_text/raw_client.py +147 -64
  28. sarvamai/speech_to_text_job/client.py +60 -15
  29. sarvamai/speech_to_text_job/raw_client.py +120 -120
  30. sarvamai/speech_to_text_streaming/__init__.py +10 -38
  31. sarvamai/speech_to_text_streaming/client.py +90 -8
  32. sarvamai/speech_to_text_streaming/raw_client.py +90 -8
  33. sarvamai/speech_to_text_streaming/types/__init__.py +8 -36
  34. sarvamai/speech_to_text_streaming/types/speech_to_text_streaming_mode.py +7 -0
  35. sarvamai/speech_to_text_streaming/types/speech_to_text_streaming_model.py +5 -0
  36. sarvamai/speech_to_text_translate_job/raw_client.py +120 -120
  37. sarvamai/speech_to_text_translate_streaming/__init__.py +5 -36
  38. sarvamai/speech_to_text_translate_streaming/client.py +8 -2
  39. sarvamai/speech_to_text_translate_streaming/raw_client.py +8 -2
  40. sarvamai/speech_to_text_translate_streaming/types/__init__.py +3 -36
  41. sarvamai/text/raw_client.py +60 -60
  42. sarvamai/text_to_speech/client.py +100 -16
  43. sarvamai/text_to_speech/raw_client.py +120 -36
  44. sarvamai/text_to_speech_streaming/__init__.py +2 -29
  45. sarvamai/text_to_speech_streaming/client.py +19 -6
  46. sarvamai/text_to_speech_streaming/raw_client.py +19 -6
  47. sarvamai/text_to_speech_streaming/types/__init__.py +3 -31
  48. sarvamai/text_to_speech_streaming/types/text_to_speech_streaming_model.py +5 -0
  49. sarvamai/types/__init__.py +102 -222
  50. sarvamai/types/chat_completion_request_message.py +2 -6
  51. sarvamai/types/configure_connection.py +4 -0
  52. sarvamai/types/configure_connection_data.py +40 -11
  53. sarvamai/types/configure_connection_data_model.py +5 -0
  54. sarvamai/types/configure_connection_data_speaker.py +35 -1
  55. sarvamai/types/error_response_data.py +1 -1
  56. sarvamai/types/file_signed_url_details.py +1 -1
  57. sarvamai/types/mode.py +5 -0
  58. sarvamai/types/speech_to_text_job_parameters.py +43 -2
  59. sarvamai/types/speech_to_text_model.py +1 -1
  60. sarvamai/types/speech_to_text_transcription_data.py +2 -2
  61. sarvamai/types/speech_to_text_translate_job_parameters.py +4 -1
  62. sarvamai/types/text_to_speech_model.py +1 -1
  63. sarvamai/types/text_to_speech_speaker.py +35 -1
  64. {sarvamai-0.1.23a3.dist-info → sarvamai-0.1.23a5.dist-info}/METADATA +1 -2
  65. {sarvamai-0.1.23a3.dist-info → sarvamai-0.1.23a5.dist-info}/RECORD +66 -66
  66. sarvamai/core/http_sse/__init__.py +0 -42
  67. sarvamai/core/http_sse/_api.py +0 -112
  68. sarvamai/core/http_sse/_decoders.py +0 -61
  69. sarvamai/core/http_sse/_exceptions.py +0 -7
  70. sarvamai/core/http_sse/_models.py +0 -17
  71. {sarvamai-0.1.23a3.dist-info → sarvamai-0.1.23a5.dist-info}/WHEEL +0 -0
@@ -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 as 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 = 1.0
21
- MAX_RETRY_DELAY_SECONDS = 60.0
22
- JITTER_FACTOR = 0.2 # 20% random jitter
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
- # 1. Check Retry-After header first
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 > 0:
108
- return min(retry_after, MAX_RETRY_DELAY_SECONDS)
76
+ if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER:
77
+ return retry_after
109
78
 
110
- # 2. Check X-RateLimit-Reset header (with positive jitter)
111
- ratelimit_reset = _parse_x_ratelimit_reset(response.headers)
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
- # 3. Fall back to exponential backoff (with symmetric jitter)
116
- backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS)
117
- return _add_symmetric_jitter(backoff)
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
- has_additional_body_parameters = bool(
219
- request_options is not None and request_options.get("additional_body_parameters")
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 = 0,
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=_build_url(base_url, path),
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=_encoded_params if _encoded_params else None,
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", 2) if request_options is not None else 2
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 retries < max_retries:
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 = 0,
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=_build_url(base_url, path),
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=_encoded_params if _encoded_params else None,
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 = 0,
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=_build_url(base_url, path),
414
+ url=urllib.parse.urljoin(f"{base_url}/", path),
542
415
  headers=jsonable_encoder(
543
416
  remove_none_from_dict(
544
417
  {
545
- **_headers,
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=_encoded_params if _encoded_params else None,
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", 2) if request_options is not None else 2
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 retries < max_retries:
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 = 0,
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=_build_url(base_url, path),
510
+ url=urllib.parse.urljoin(f"{base_url}/", path),
647
511
  headers=jsonable_encoder(
648
512
  remove_none_from_dict(
649
513
  {
650
- **_headers,
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=_encoded_params if _encoded_params else None,
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,
@@ -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
- # convert_and_respect_annotation_metadata is required for TypedDict aliasing.
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.dict() # type: ignore[attr-defined]
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 cast(
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
- # In Pydantic v2, for RootModel we always use "before" mode
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