google-genai 1.6.0__py3-none-any.whl → 1.8.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.
google/genai/_common.py CHANGED
@@ -20,7 +20,7 @@ import datetime
20
20
  import enum
21
21
  import functools
22
22
  import typing
23
- from typing import Union
23
+ from typing import Any, Union
24
24
  import uuid
25
25
  import warnings
26
26
 
@@ -93,7 +93,7 @@ def set_value_by_path(data, keys, value):
93
93
  data[keys[-1]] = value
94
94
 
95
95
 
96
- def get_value_by_path(data: object, keys: list[str]):
96
+ def get_value_by_path(data: Any, keys: list[str]):
97
97
  """Examples:
98
98
 
99
99
  get_value_by_path({'a': {'b': v}}, ['a', 'b'])
@@ -128,7 +128,7 @@ def get_value_by_path(data: object, keys: list[str]):
128
128
  return data
129
129
 
130
130
 
131
- def convert_to_dict(obj: dict[str, object]) -> dict[str, object]:
131
+ def convert_to_dict(obj: object) -> Any:
132
132
  """Recursively converts a given object to a dictionary.
133
133
 
134
134
  If the object is a Pydantic model, it uses the model's `model_dump()` method.
@@ -137,7 +137,9 @@ def convert_to_dict(obj: dict[str, object]) -> dict[str, object]:
137
137
  obj: The object to convert.
138
138
 
139
139
  Returns:
140
- A dictionary representation of the object.
140
+ A dictionary representation of the object, a list of objects if a list is
141
+ passed, or the object itself if it is not a dictionary, list, or Pydantic
142
+ model.
141
143
  """
142
144
  if isinstance(obj, pydantic.BaseModel):
143
145
  return obj.model_dump(exclude_none=True)
@@ -150,7 +152,7 @@ def convert_to_dict(obj: dict[str, object]) -> dict[str, object]:
150
152
 
151
153
 
152
154
  def _remove_extra_fields(
153
- model: pydantic.BaseModel, response: dict[str, object]
155
+ model: Any, response: dict[str, object]
154
156
  ) -> None:
155
157
  """Removes extra fields from the response that are not in the model.
156
158
 
@@ -119,7 +119,8 @@ def _redact_request_body(body: dict[str, object]):
119
119
  def redact_http_request(http_request: HttpRequest):
120
120
  http_request.headers = _redact_request_headers(http_request.headers)
121
121
  http_request.url = _redact_request_url(http_request.url)
122
- _redact_request_body(http_request.data)
122
+ if not isinstance(http_request.data, bytes):
123
+ _redact_request_body(http_request.data)
123
124
 
124
125
 
125
126
  def _current_file_path_and_line():
@@ -321,6 +322,8 @@ class ReplayApiClient(BaseApiClient):
321
322
  raise ValueError(
322
323
  'Unsupported http_response type: ' + str(type(http_response))
323
324
  )
325
+ if self.replay_session is None:
326
+ raise ValueError('No replay session found.')
324
327
  self.replay_session.interactions.append(
325
328
  ReplayInteraction(request=request, response=response)
326
329
  )
@@ -342,7 +345,8 @@ class ReplayApiClient(BaseApiClient):
342
345
  request_data_copy = copy.deepcopy(http_request.data)
343
346
  # Both the request and recorded request must be redacted before comparing
344
347
  # so that the comparison is fair.
345
- _redact_request_body(request_data_copy)
348
+ if not isinstance(request_data_copy, bytes):
349
+ _redact_request_body(request_data_copy)
346
350
 
347
351
  actual_request_body = [request_data_copy]
348
352
  expected_request_body = interaction.request.body_segments
@@ -352,9 +356,11 @@ class ReplayApiClient(BaseApiClient):
352
356
  f'Expected: {expected_request_body}'
353
357
  )
354
358
 
355
- def _build_response_from_replay(self, http_request: HttpRequest):
359
+ def _build_response_from_replay(self, http_request: HttpRequest) -> HttpResponse:
356
360
  redact_http_request(http_request)
357
361
 
362
+ if self.replay_session is None:
363
+ raise ValueError('No replay session found.')
358
364
  interaction = self.replay_session.interactions[self._replay_index]
359
365
  # Replay is on the right side of the assert so the diff makes more sense.
360
366
  self._match_request(http_request, interaction)
@@ -373,6 +379,8 @@ class ReplayApiClient(BaseApiClient):
373
379
  def _verify_response(self, response_model: BaseModel):
374
380
  if self._mode == 'api':
375
381
  return
382
+ if not self.replay_session:
383
+ raise ValueError('No replay session found.')
376
384
  # replay_index is advanced in _build_response_from_replay, so we need to -1.
377
385
  interaction = self.replay_session.interactions[self._replay_index - 1]
378
386
  if self._should_update_replay():
@@ -453,7 +461,7 @@ class ReplayApiClient(BaseApiClient):
453
461
  else:
454
462
  return self._build_response_from_replay(http_request)
455
463
 
456
- def upload_file(self, file_path: Union[str, io.IOBase], upload_url: str, upload_size: int):
464
+ def upload_file(self, file_path: Union[str, io.IOBase], upload_url: str, upload_size: int) -> HttpResponse:
457
465
  if isinstance(file_path, io.IOBase):
458
466
  offset = file_path.tell()
459
467
  content = file_path.read()
@@ -474,21 +482,21 @@ class ReplayApiClient(BaseApiClient):
474
482
  result = super().upload_file(file_path, upload_url, upload_size)
475
483
  except HTTPError as e:
476
484
  result = HttpResponse(
477
- e.response.headers, [json.dumps({'reason': e.response.reason})]
485
+ dict(e.response.headers), [json.dumps({'reason': e.response.reason})]
478
486
  )
479
487
  result.status_code = e.response.status_code
480
488
  raise e
481
- self._record_interaction(request, HttpResponse({}, [json.dumps(result)]))
489
+ self._record_interaction(request, result)
482
490
  return result
483
491
  else:
484
- return self._build_response_from_replay(request).json
492
+ return self._build_response_from_replay(request)
485
493
 
486
494
  async def async_upload_file(
487
495
  self,
488
496
  file_path: Union[str, io.IOBase],
489
497
  upload_url: str,
490
498
  upload_size: int,
491
- ) -> str:
499
+ ) -> HttpResponse:
492
500
  if isinstance(file_path, io.IOBase):
493
501
  offset = file_path.tell()
494
502
  content = file_path.read()
@@ -504,37 +512,40 @@ class ReplayApiClient(BaseApiClient):
504
512
  method='POST', url='', data={'file_path': file_path}, headers={}
505
513
  )
506
514
  if self._should_call_api():
507
- result: Union[str, HttpResponse]
515
+ result: HttpResponse
508
516
  try:
509
517
  result = await super().async_upload_file(
510
518
  file_path, upload_url, upload_size
511
519
  )
512
520
  except HTTPError as e:
513
521
  result = HttpResponse(
514
- e.response.headers, [json.dumps({'reason': e.response.reason})]
522
+ dict(e.response.headers), [json.dumps({'reason': e.response.reason})]
515
523
  )
516
524
  result.status_code = e.response.status_code
517
525
  raise e
518
- self._record_interaction(request, HttpResponse({}, [json.dumps(result)]))
526
+ self._record_interaction(request, result)
519
527
  return result
520
528
  else:
521
- return self._build_response_from_replay(request).json
529
+ return self._build_response_from_replay(request)
522
530
 
523
- def _download_file_request(self, request):
531
+ def download_file(self, path: str, http_options: HttpOptions):
524
532
  self._initialize_replay_session_if_not_loaded()
533
+ request = self._build_request(
534
+ 'get', path=path, request_dict={}, http_options=http_options
535
+ )
525
536
  if self._should_call_api():
526
537
  try:
527
- result = super()._download_file_request(request)
538
+ result = super().download_file(path, http_options)
528
539
  except HTTPError as e:
529
540
  result = HttpResponse(
530
- e.response.headers, [json.dumps({'reason': e.response.reason})]
541
+ dict(e.response.headers), [json.dumps({'reason': e.response.reason})]
531
542
  )
532
543
  result.status_code = e.response.status_code
533
544
  raise e
534
545
  self._record_interaction(request, result)
535
546
  return result
536
547
  else:
537
- return self._build_response_from_replay(request)
548
+ return self._build_response_from_replay(request).byte_stream[0]
538
549
 
539
550
  async def async_download_file(self, path: str, http_options):
540
551
  self._initialize_replay_session_if_not_loaded()
@@ -546,7 +557,7 @@ class ReplayApiClient(BaseApiClient):
546
557
  result = await super().async_download_file(path, http_options)
547
558
  except HTTPError as e:
548
559
  result = HttpResponse(
549
- e.response.headers, [json.dumps({'reason': e.response.reason})]
560
+ dict(e.response.headers), [json.dumps({'reason': e.response.reason})]
550
561
  )
551
562
  result.status_code = e.response.status_code
552
563
  raise e
@@ -181,17 +181,26 @@ def t_models_url(
181
181
 
182
182
  def t_extract_models(
183
183
  api_client: _api_client.BaseApiClient,
184
- response: dict[str, list[types.ModelDict]],
185
- ) -> Optional[list[types.ModelDict]]:
184
+ response: dict[str, Any],
185
+ ) -> list[dict[str, Any]]:
186
186
  if not response:
187
187
  return []
188
- elif response.get('models') is not None:
189
- return response.get('models')
190
- elif response.get('tunedModels') is not None:
191
- return response.get('tunedModels')
192
- elif response.get('publisherModels') is not None:
193
- return response.get('publisherModels')
194
- elif (
188
+
189
+ models: Optional[list[dict[str, Any]]] = response.get('models')
190
+ if models is not None:
191
+ return models
192
+
193
+ tuned_models: Optional[list[dict[str, Any]]] = response.get('tunedModels')
194
+ if tuned_models is not None:
195
+ return tuned_models
196
+
197
+ publisher_models: Optional[list[dict[str, Any]]] = response.get(
198
+ 'publisherModels'
199
+ )
200
+ if publisher_models is not None:
201
+ return publisher_models
202
+
203
+ if (
195
204
  response.get('httpHeaders') is not None
196
205
  and response.get('jsonPayload') is None
197
206
  ):
@@ -526,7 +535,6 @@ def process_schema(
526
535
  ):
527
536
  """Updates the schema and each sub-schema inplace to be API-compatible.
528
537
 
529
- - Removes the `title` field from the schema if the client is not vertexai.
530
538
  - Inlines the $defs.
531
539
 
532
540
  Example of a schema before and after (with mldev):
@@ -570,21 +578,22 @@ def process_schema(
570
578
  'items': {
571
579
  'properties': {
572
580
  'continent': {
573
- 'type': 'string'
581
+ 'title': 'Continent',
582
+ 'type': 'string'
574
583
  },
575
584
  'gdp': {
576
- 'type': 'integer'}
585
+ 'title': 'Gdp',
586
+ 'type': 'integer'
577
587
  },
578
588
  }
579
589
  'required':['continent', 'gdp'],
590
+ 'title': 'CountryInfo',
580
591
  'type': 'object'
581
592
  },
582
593
  'type': 'array'
583
594
  }
584
595
  """
585
596
  if not client.vertexai:
586
- schema.pop('title', None)
587
-
588
597
  if schema.get('default') is not None:
589
598
  raise ValueError(
590
599
  'Default value is not supported in the response schema for the Gemini'