google-genai 1.41.0__py3-none-any.whl → 1.42.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.
@@ -80,7 +80,7 @@ if TYPE_CHECKING:
80
80
 
81
81
  logger = logging.getLogger('google_genai._api_client')
82
82
  CHUNK_SIZE = 8 * 1024 * 1024 # 8 MB chunk size
83
- READ_BUFFER_SIZE = 2**20
83
+ READ_BUFFER_SIZE = 2**22
84
84
  MAX_RETRY_COUNT = 3
85
85
  INITIAL_RETRY_DELAY = 1 # second
86
86
  DELAY_MULTIPLIER = 2
@@ -489,6 +489,7 @@ def retry_args(options: Optional[HttpRetryOptions]) -> _common.StringDict:
489
489
  'retry': retry,
490
490
  'reraise': True,
491
491
  'wait': wait,
492
+ 'before_sleep': tenacity.before_sleep_log(logger, logging.INFO),
492
493
  }
493
494
 
494
495
 
google/genai/_common.py CHANGED
@@ -21,6 +21,7 @@ import datetime
21
21
  import enum
22
22
  import functools
23
23
  import logging
24
+ import re
24
25
  import typing
25
26
  from typing import Any, Callable, FrozenSet, Optional, Union, get_args, get_origin
26
27
  import uuid
@@ -38,7 +39,9 @@ class ExperimentalWarning(Warning):
38
39
  """Warning for experimental features."""
39
40
 
40
41
 
41
- def set_value_by_path(data: Optional[dict[Any, Any]], keys: list[str], value: Any) -> None:
42
+ def set_value_by_path(
43
+ data: Optional[dict[Any, Any]], keys: list[str], value: Any
44
+ ) -> None:
42
45
  """Examples:
43
46
 
44
47
  set_value_by_path({}, ['a', 'b'], v)
@@ -100,14 +103,19 @@ def set_value_by_path(data: Optional[dict[Any, Any]], keys: list[str], value: An
100
103
  f' Existing value: {existing_data}; New value: {value}.'
101
104
  )
102
105
  else:
103
- if (keys[-1] == '_self' and isinstance(data, dict)
104
- and isinstance(value, dict)):
106
+ if (
107
+ keys[-1] == '_self'
108
+ and isinstance(data, dict)
109
+ and isinstance(value, dict)
110
+ ):
105
111
  data.update(value)
106
112
  else:
107
113
  data[keys[-1]] = value
108
114
 
109
115
 
110
- def get_value_by_path(data: Any, keys: list[str]) -> Any:
116
+ def get_value_by_path(
117
+ data: Any, keys: list[str], *, default_value: Any = None
118
+ ) -> Any:
111
119
  """Examples:
112
120
 
113
121
  get_value_by_path({'a': {'b': v}}, ['a', 'b'])
@@ -119,36 +127,141 @@ def get_value_by_path(data: Any, keys: list[str]) -> Any:
119
127
  return data
120
128
  for i, key in enumerate(keys):
121
129
  if not data:
122
- return None
130
+ return default_value
123
131
  if key.endswith('[]'):
124
132
  key_name = key[:-2]
125
133
  if key_name in data:
126
- return [get_value_by_path(d, keys[i + 1 :]) for d in data[key_name]]
134
+ return [
135
+ get_value_by_path(d, keys[i + 1 :], default_value=default_value)
136
+ for d in data[key_name]
137
+ ]
127
138
  else:
128
- return None
139
+ return default_value
129
140
  elif key.endswith('[0]'):
130
141
  key_name = key[:-3]
131
142
  if key_name in data and data[key_name]:
132
- return get_value_by_path(data[key_name][0], keys[i + 1 :])
143
+ return get_value_by_path(
144
+ data[key_name][0], keys[i + 1 :], default_value=default_value
145
+ )
133
146
  else:
134
- return None
147
+ return default_value
135
148
  else:
136
149
  if key in data:
137
150
  data = data[key]
138
151
  elif isinstance(data, BaseModel) and hasattr(data, key):
139
152
  data = getattr(data, key)
140
153
  else:
141
- return None
154
+ return default_value
142
155
  return data
143
156
 
144
157
 
145
- def convert_to_dict(obj: object) -> Any:
158
+ def move_value_by_path(data: Any, paths: dict[str, str]) -> None:
159
+ """Moves values from source paths to destination paths.
160
+
161
+ Examples:
162
+ move_value_by_path(
163
+ {'requests': [{'content': v1}, {'content': v2}]},
164
+ {'requests[].*': 'requests[].request.*'}
165
+ )
166
+ -> {'requests': [{'request': {'content': v1}}, {'request': {'content':
167
+ v2}}]}
168
+ """
169
+ for source_path, dest_path in paths.items():
170
+ source_keys = source_path.split('.')
171
+ dest_keys = dest_path.split('.')
172
+
173
+ # Determine keys to exclude from wildcard to avoid cyclic references
174
+ exclude_keys = set()
175
+ wildcard_idx = -1
176
+ for i, key in enumerate(source_keys):
177
+ if key == '*':
178
+ wildcard_idx = i
179
+ break
180
+
181
+ if wildcard_idx != -1 and len(dest_keys) > wildcard_idx:
182
+ # Extract the intermediate key between source and dest paths
183
+ # Example: source=['requests[]', '*'], dest=['requests[]', 'request', '*']
184
+ # We want to exclude 'request'
185
+ for i in range(wildcard_idx, len(dest_keys)):
186
+ key = dest_keys[i]
187
+ if key != '*' and not key.endswith('[]') and not key.endswith('[0]'):
188
+ exclude_keys.add(key)
189
+
190
+ # Move values recursively
191
+ _move_value_recursive(data, source_keys, dest_keys, 0, exclude_keys)
192
+
193
+
194
+ def _move_value_recursive(
195
+ data: Any,
196
+ source_keys: list[str],
197
+ dest_keys: list[str],
198
+ key_idx: int,
199
+ exclude_keys: set[str],
200
+ ) -> None:
201
+ """Recursively moves values from source path to destination path."""
202
+ if key_idx >= len(source_keys):
203
+ return
204
+
205
+ key = source_keys[key_idx]
206
+
207
+ if key.endswith('[]'):
208
+ # Handle array iteration
209
+ key_name = key[:-2]
210
+ if key_name in data and isinstance(data[key_name], list):
211
+ for item in data[key_name]:
212
+ _move_value_recursive(
213
+ item, source_keys, dest_keys, key_idx + 1, exclude_keys
214
+ )
215
+ elif key == '*':
216
+ # Handle wildcard - move all fields
217
+ if isinstance(data, dict):
218
+ # Get all keys to move (excluding specified keys)
219
+ keys_to_move = [
220
+ k
221
+ for k in list(data.keys())
222
+ if not k.startswith('_') and k not in exclude_keys
223
+ ]
224
+
225
+ # Collect values to move
226
+ values_to_move = {k: data[k] for k in keys_to_move}
227
+
228
+ # Set values at destination
229
+ for k, v in values_to_move.items():
230
+ # Build destination keys with the field name
231
+ new_dest_keys = []
232
+ for dk in dest_keys[key_idx:]:
233
+ if dk == '*':
234
+ new_dest_keys.append(k)
235
+ else:
236
+ new_dest_keys.append(dk)
237
+ set_value_by_path(data, new_dest_keys, v)
238
+
239
+ # Delete from source
240
+ for k in keys_to_move:
241
+ del data[k]
242
+ else:
243
+ # Navigate to next level
244
+ if key in data:
245
+ _move_value_recursive(
246
+ data[key], source_keys, dest_keys, key_idx + 1, exclude_keys
247
+ )
248
+
249
+
250
+ def maybe_snake_to_camel(snake_str: str, convert: bool = True) -> str:
251
+ """Converts a snake_case string to CamelCase, if convert is True."""
252
+ if not convert:
253
+ return snake_str
254
+ return re.sub(r'_([a-zA-Z])', lambda match: match.group(1).upper(), snake_str)
255
+
256
+
257
+ def convert_to_dict(obj: object, convert_keys: bool = False) -> Any:
146
258
  """Recursively converts a given object to a dictionary.
147
259
 
148
260
  If the object is a Pydantic model, it uses the model's `model_dump()` method.
149
261
 
150
262
  Args:
151
263
  obj: The object to convert.
264
+ convert_keys: Whether to convert the keys from snake case to camel case.
152
265
 
153
266
  Returns:
154
267
  A dictionary representation of the object, a list of objects if a list is
@@ -156,17 +269,23 @@ def convert_to_dict(obj: object) -> Any:
156
269
  model.
157
270
  """
158
271
  if isinstance(obj, pydantic.BaseModel):
159
- return obj.model_dump(exclude_none=True)
272
+ return convert_to_dict(obj.model_dump(exclude_none=True), convert_keys)
160
273
  elif isinstance(obj, dict):
161
- return {key: convert_to_dict(value) for key, value in obj.items()}
274
+ return {
275
+ maybe_snake_to_camel(key, convert_keys): convert_to_dict(
276
+ value, convert_keys
277
+ )
278
+ for key, value in obj.items()
279
+ }
162
280
  elif isinstance(obj, list):
163
- return [convert_to_dict(item) for item in obj]
281
+ return [convert_to_dict(item, convert_keys) for item in obj]
164
282
  else:
165
283
  return obj
166
284
 
167
285
 
168
286
  def _is_struct_type(annotation: type) -> bool:
169
287
  """Checks if the given annotation is list[dict[str, typing.Any]]
288
+
170
289
  or typing.List[typing.Dict[str, typing.Any]].
171
290
 
172
291
  This maps to Struct type in the API.
@@ -174,7 +293,7 @@ def _is_struct_type(annotation: type) -> bool:
174
293
  outer_origin = get_origin(annotation)
175
294
  outer_args = get_args(annotation)
176
295
 
177
- if outer_origin is not list: # Python 3.9+ normalizes list
296
+ if outer_origin is not list: # Python 3.9+ normalizes list
178
297
  return False
179
298
 
180
299
  if not outer_args or len(outer_args) != 1:
@@ -185,7 +304,7 @@ def _is_struct_type(annotation: type) -> bool:
185
304
  inner_origin = get_origin(inner_annotation)
186
305
  inner_args = get_args(inner_annotation)
187
306
 
188
- if inner_origin is not dict: # Python 3.9+ normalizes to dict
307
+ if inner_origin is not dict: # Python 3.9+ normalizes to dict
189
308
  return False
190
309
 
191
310
  if not inner_args or len(inner_args) != 2:
@@ -197,9 +316,7 @@ def _is_struct_type(annotation: type) -> bool:
197
316
  return key_type is str and value_type is typing.Any
198
317
 
199
318
 
200
- def _remove_extra_fields(
201
- model: Any, response: dict[str, object]
202
- ) -> None:
319
+ def _remove_extra_fields(model: Any, response: dict[str, object]) -> None:
203
320
  """Removes extra fields from the response that are not in the model.
204
321
 
205
322
  Mutates the response in place.
@@ -239,6 +356,7 @@ def _remove_extra_fields(
239
356
  if isinstance(item, dict):
240
357
  _remove_extra_fields(typing.get_args(annotation)[0], item)
241
358
 
359
+
242
360
  T = typing.TypeVar('T', bound='BaseModel')
243
361
 
244
362
 
@@ -378,56 +496,57 @@ def _format_collection(
378
496
  depth: int,
379
497
  visited: FrozenSet[int],
380
498
  ) -> str:
381
- """Formats a collection (list, tuple, set)."""
382
- if isinstance(obj, list):
383
- brackets = ('[', ']')
384
- internal_obj = obj
385
- elif isinstance(obj, tuple):
386
- brackets = ('(', ')')
387
- internal_obj = list(obj)
388
- elif isinstance(obj, set):
389
- internal_obj = list(obj)
390
- if obj:
391
- brackets = ('{', '}')
392
- else:
393
- brackets = ('set(', ')')
499
+ """Formats a collection (list, tuple, set)."""
500
+ if isinstance(obj, list):
501
+ brackets = ('[', ']')
502
+ internal_obj = obj
503
+ elif isinstance(obj, tuple):
504
+ brackets = ('(', ')')
505
+ internal_obj = list(obj)
506
+ elif isinstance(obj, set):
507
+ internal_obj = list(obj)
508
+ if obj:
509
+ brackets = ('{', '}')
394
510
  else:
395
- raise ValueError(f"Unsupported collection type: {type(obj)}")
511
+ brackets = ('set(', ')')
512
+ else:
513
+ raise ValueError(f'Unsupported collection type: {type(obj)}')
396
514
 
397
- if not internal_obj:
398
- return brackets[0] + brackets[1]
515
+ if not internal_obj:
516
+ return brackets[0] + brackets[1]
399
517
 
400
- # If the call to _pretty_repr for elements will have depth < 0
401
- if depth <= 0:
402
- item_count_str = f"{len(internal_obj)} item{'s'*(len(internal_obj)!=1)}"
403
- return f'{brackets[0]}<... {item_count_str} at Max depth ...>{brackets[1]}'
404
-
405
- indent = ' ' * indent_level
406
- next_indent_str = ' ' * (indent_level + indent_delta)
407
- elements = []
408
- num_to_show = min(len(internal_obj), max_items)
409
-
410
- for i in range(num_to_show):
411
- elem = internal_obj[i]
412
- elements.append(
413
- next_indent_str
414
- + _pretty_repr(
415
- elem,
416
- indent_level=indent_level + indent_delta,
417
- indent_delta=indent_delta,
418
- max_len=max_len,
419
- max_items=max_items,
420
- depth=depth - 1,
421
- visited=visited,
422
- )
423
- )
518
+ # If the call to _pretty_repr for elements will have depth < 0
519
+ if depth <= 0:
520
+ item_count_str = f"{len(internal_obj)} item{'s'*(len(internal_obj)!=1)}"
521
+ return f'{brackets[0]}<... {item_count_str} at Max depth ...>{brackets[1]}'
424
522
 
425
- if len(internal_obj) > max_items:
426
- elements.append(
427
- f'{next_indent_str}<... {len(internal_obj) - max_items} more items ...>'
523
+ indent = ' ' * indent_level
524
+ next_indent_str = ' ' * (indent_level + indent_delta)
525
+ elements = []
526
+ num_to_show = min(len(internal_obj), max_items)
527
+
528
+ for i in range(num_to_show):
529
+ elem = internal_obj[i]
530
+ elements.append(
531
+ next_indent_str
532
+ + _pretty_repr(
533
+ elem,
534
+ indent_level=indent_level + indent_delta,
535
+ indent_delta=indent_delta,
536
+ max_len=max_len,
537
+ max_items=max_items,
538
+ depth=depth - 1,
539
+ visited=visited,
428
540
  )
541
+ )
542
+
543
+ if len(internal_obj) > max_items:
544
+ elements.append(
545
+ f'{next_indent_str}<... {len(internal_obj) - max_items} more items ...>'
546
+ )
547
+
548
+ return f'{brackets[0]}\n' + ',\n'.join(elements) + f',\n{indent}{brackets[1]}'
429
549
 
430
- return f'{brackets[0]}\n' + ',\n'.join(elements) + f',\n{indent}{brackets[1]}'
431
550
 
432
551
  class BaseModel(pydantic.BaseModel):
433
552
 
@@ -441,7 +560,7 @@ class BaseModel(pydantic.BaseModel):
441
560
  arbitrary_types_allowed=True,
442
561
  ser_json_bytes='base64',
443
562
  val_json_bytes='base64',
444
- ignored_types=(typing.TypeVar,)
563
+ ignored_types=(typing.TypeVar,),
445
564
  )
446
565
 
447
566
  def __repr__(self) -> str:
@@ -452,7 +571,10 @@ class BaseModel(pydantic.BaseModel):
452
571
 
453
572
  @classmethod
454
573
  def _from_response(
455
- cls: typing.Type[T], *, response: dict[str, object], kwargs: dict[str, object]
574
+ cls: typing.Type[T],
575
+ *,
576
+ response: dict[str, object],
577
+ kwargs: dict[str, object],
456
578
  ) -> T:
457
579
  # To maintain forward compatibility, we need to remove extra fields from
458
580
  # the response.
@@ -462,11 +584,11 @@ class BaseModel(pydantic.BaseModel):
462
584
  # user may pass a dict that is not a subclass of BaseModel.
463
585
  # If more modules require we skip this, we may want a different approach
464
586
  should_skip_removing_fields = (
465
- kwargs is not None and
466
- 'config' in kwargs and
467
- kwargs['config'] is not None and
468
- isinstance(kwargs['config'], dict) and
469
- 'include_all_fields' in kwargs['config']
587
+ kwargs is not None
588
+ and 'config' in kwargs
589
+ and kwargs['config'] is not None
590
+ and isinstance(kwargs['config'], dict)
591
+ and 'include_all_fields' in kwargs['config']
470
592
  and kwargs['config']['include_all_fields']
471
593
  )
472
594
 
@@ -490,7 +612,7 @@ class CaseInSensitiveEnum(str, enum.Enum):
490
612
  try:
491
613
  return cls[value.lower()] # Try to access directly with lowercase
492
614
  except KeyError:
493
- warnings.warn(f"{value} is not a valid {cls.__name__}")
615
+ warnings.warn(f'{value} is not a valid {cls.__name__}')
494
616
  try:
495
617
  # Creating a enum instance based on the value
496
618
  # We need to use super() to avoid infinite recursion.
@@ -551,10 +673,14 @@ def encode_unserializable_types(data: dict[str, object]) -> dict[str, object]:
551
673
  return processed_data
552
674
 
553
675
 
554
- def experimental_warning(message: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
676
+ def experimental_warning(
677
+ message: str,
678
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
555
679
  """Experimental warning, only warns once."""
680
+
556
681
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
557
682
  warning_done = False
683
+
558
684
  @functools.wraps(func)
559
685
  def wrapper(*args: Any, **kwargs: Any) -> Any:
560
686
  nonlocal warning_done
@@ -566,13 +692,15 @@ def experimental_warning(message: str) -> Callable[[Callable[..., Any]], Callabl
566
692
  stacklevel=2,
567
693
  )
568
694
  return func(*args, **kwargs)
695
+
569
696
  return wrapper
697
+
570
698
  return decorator
571
699
 
572
700
 
573
701
  def _normalize_key_for_matching(key_str: str) -> str:
574
702
  """Normalizes a key for case-insensitive and snake/camel matching."""
575
- return key_str.replace("_", "").lower()
703
+ return key_str.replace('_', '').lower()
576
704
 
577
705
 
578
706
  def align_key_case(
@@ -588,7 +716,9 @@ def align_key_case(
588
716
  A new dictionary with keys aligned to target_dict's key casing.
589
717
  """
590
718
  aligned_update_dict: StringDict = {}
591
- target_keys_map = {_normalize_key_for_matching(key): key for key in target_dict.keys()}
719
+ target_keys_map = {
720
+ _normalize_key_for_matching(key): key for key in target_dict.keys()
721
+ }
592
722
 
593
723
  for key, value in update_dict.items():
594
724
  normalized_update_key = _normalize_key_for_matching(key)
@@ -598,9 +728,15 @@ def align_key_case(
598
728
  else:
599
729
  aligned_key = key
600
730
 
601
- if isinstance(value, dict) and isinstance(target_dict.get(aligned_key), dict):
602
- aligned_update_dict[aligned_key] = align_key_case(target_dict[aligned_key], value)
603
- elif isinstance(value, list) and isinstance(target_dict.get(aligned_key), list):
731
+ if isinstance(value, dict) and isinstance(
732
+ target_dict.get(aligned_key), dict
733
+ ):
734
+ aligned_update_dict[aligned_key] = align_key_case(
735
+ target_dict[aligned_key], value
736
+ )
737
+ elif isinstance(value, list) and isinstance(
738
+ target_dict.get(aligned_key), list
739
+ ):
604
740
  # Direct assign as we treat update_dict list values as golden source.
605
741
  aligned_update_dict[aligned_key] = value
606
742
  else: