google-genai 1.19.0__py3-none-any.whl → 1.21.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
@@ -19,6 +19,7 @@ import base64
19
19
  import datetime
20
20
  import enum
21
21
  import functools
22
+ import logging
22
23
  import typing
23
24
  from typing import Any, Callable, Optional, Union, get_origin, get_args
24
25
  import uuid
@@ -30,6 +31,7 @@ from pydantic import alias_generators
30
31
  from . import _api_client
31
32
  from . import errors
32
33
 
34
+ logger = logging.getLogger('google_genai._common')
33
35
 
34
36
  def set_value_by_path(data: Optional[dict[Any, Any]], keys: list[str], value: Any) -> None:
35
37
  """Examples:
@@ -253,7 +255,21 @@ class BaseModel(pydantic.BaseModel):
253
255
  # To maintain forward compatibility, we need to remove extra fields from
254
256
  # the response.
255
257
  # We will provide another mechanism to allow users to access these fields.
256
- _remove_extra_fields(cls, response)
258
+
259
+ # For Agent Engine we don't want to call _remove_all_fields because the
260
+ # user may pass a dict that is not a subclass of BaseModel.
261
+ # If more modules require we skip this, we may want a different approach
262
+ should_skip_removing_fields = (
263
+ kwargs is not None and
264
+ 'config' in kwargs and
265
+ kwargs['config'] is not None and
266
+ isinstance(kwargs['config'], dict) and
267
+ 'include_all_fields' in kwargs['config']
268
+ and kwargs['config']['include_all_fields']
269
+ )
270
+
271
+ if not should_skip_removing_fields:
272
+ _remove_extra_fields(cls, response)
257
273
  validated_response = cls.model_validate(response)
258
274
  return validated_response
259
275
 
@@ -351,3 +367,74 @@ def experimental_warning(message: str) -> Callable[[Callable[..., Any]], Callabl
351
367
  return wrapper
352
368
  return decorator
353
369
 
370
+
371
+ def _normalize_key_for_matching(key_str: str) -> str:
372
+ """Normalizes a key for case-insensitive and snake/camel matching."""
373
+ return key_str.replace("_", "").lower()
374
+
375
+
376
+ def align_key_case(target_dict: dict[str, Any], update_dict: dict[str, Any]) -> dict[str, Any]:
377
+ """Aligns the keys of update_dict to the case of target_dict keys.
378
+
379
+ Args:
380
+ target_dict: The dictionary with the target key casing.
381
+ update_dict: The dictionary whose keys need to be aligned.
382
+
383
+ Returns:
384
+ A new dictionary with keys aligned to target_dict's key casing.
385
+ """
386
+ aligned_update_dict: dict[str, Any] = {}
387
+ target_keys_map = {_normalize_key_for_matching(key): key for key in target_dict.keys()}
388
+
389
+ for key, value in update_dict.items():
390
+ normalized_update_key = _normalize_key_for_matching(key)
391
+
392
+ if normalized_update_key in target_keys_map:
393
+ aligned_key = target_keys_map[normalized_update_key]
394
+ else:
395
+ aligned_key = key
396
+
397
+ if isinstance(value, dict) and isinstance(target_dict.get(aligned_key), dict):
398
+ aligned_update_dict[aligned_key] = align_key_case(target_dict[aligned_key], value)
399
+ elif isinstance(value, list) and isinstance(target_dict.get(aligned_key), list):
400
+ # Direct assign as we treat update_dict list values as golden source.
401
+ aligned_update_dict[aligned_key] = value
402
+ else:
403
+ aligned_update_dict[aligned_key] = value
404
+ return aligned_update_dict
405
+
406
+
407
+ def recursive_dict_update(
408
+ target_dict: dict[str, Any], update_dict: dict[str, Any]
409
+ ) -> None:
410
+ """Recursively updates a target dictionary with values from an update dictionary.
411
+
412
+ We don't enforce the updated dict values to have the same type with the
413
+ target_dict values except log warnings.
414
+ Users providing the update_dict should be responsible for constructing correct
415
+ data.
416
+
417
+ Args:
418
+ target_dict (dict): The dictionary to be updated.
419
+ update_dict (dict): The dictionary containing updates.
420
+ """
421
+ # Python SDK http request may change in camel case or snake case:
422
+ # If the field is directly set via setv() function, then it is camel case;
423
+ # otherwise it is snake case.
424
+ # Align the update_dict key case to target_dict to ensure correct dict update.
425
+ aligned_update_dict = align_key_case(target_dict, update_dict)
426
+ for key, value in aligned_update_dict.items():
427
+ if (
428
+ key in target_dict
429
+ and isinstance(target_dict[key], dict)
430
+ and isinstance(value, dict)
431
+ ):
432
+ recursive_dict_update(target_dict[key], value)
433
+ elif key in target_dict and not isinstance(target_dict[key], type(value)):
434
+ logger.warning(
435
+ f"Type mismatch for key '{key}'. Existing type:"
436
+ f' {type(target_dict[key])}, new type: {type(value)}. Overwriting.'
437
+ )
438
+ target_dict[key] = value
439
+ else:
440
+ target_dict[key] = value