2929from typing import Any , GenericAlias , List , Optional , Sequence , Union # type: ignore[attr-defined]
3030from ._mcp_utils import mcp_to_gemini_tool
3131from ._common import get_value_by_path as getv
32+ from ._common import is_duck_type_of
3233
3334if typing .TYPE_CHECKING :
3435 import PIL .Image
7273metric_name_api_sdk_map = {v : k for k , v in metric_name_sdk_api_map .items ()}
7374
7475
75- def _is_duck_type_of (obj : Any , cls : type [pydantic .BaseModel ]) -> bool :
76- """Checks if an object has all of the fields of a Pydantic model.
77-
78- This is a duck-typing alternative to `isinstance` to solve dual-import
79- problems. It returns False for dictionaries, which should be handled by
80- `isinstance(obj, dict)`.
81-
82- Args:
83- obj: The object to check.
84- cls: The Pydantic model class to duck-type against.
85-
86- Returns:
87- True if the object has all the fields defined in the Pydantic model, False
88- otherwise.
89- """
90- if isinstance (obj , dict ) or not hasattr (cls , 'model_fields' ):
91- return False
92-
93- # Check if the object has all of the Pydantic model's defined fields.
94- all_matched = all (hasattr (obj , field ) for field in cls .model_fields )
95- if not all_matched and isinstance (obj , pydantic .BaseModel ):
96- # Check the other way around if obj is a Pydantic model.
97- # Check if the Pydantic model has all of the object's defined fields.
98- try :
99- obj_private = cls ()
100- all_matched = all (hasattr (obj_private , f ) for f in type (obj ).model_fields )
101- except ValueError :
102- return False
103- return all_matched
104-
105-
10676def _resource_name (
10777 client : _api_client .BaseApiClient ,
10878 resource_name : str ,
@@ -311,7 +281,7 @@ def t_function_response(
311281 raise ValueError ('function_response is required.' )
312282 if isinstance (function_response , dict ):
313283 return types .FunctionResponse .model_validate (function_response )
314- elif _is_duck_type_of (function_response , types .FunctionResponse ):
284+ elif is_duck_type_of (function_response , types .FunctionResponse ):
315285 return function_response
316286 else :
317287 raise TypeError (
@@ -347,7 +317,7 @@ def t_blob(blob: types.BlobImageUnionDict) -> types.Blob:
347317 if not blob :
348318 raise ValueError ('blob is required.' )
349319
350- if _is_duck_type_of (blob , types .Blob ):
320+ if is_duck_type_of (blob , types .Blob ):
351321 return blob # type: ignore[return-value]
352322
353323 if isinstance (blob , dict ):
@@ -388,7 +358,7 @@ def t_part(part: Optional[types.PartUnionDict]) -> types.Part:
388358 raise ValueError ('content part is required.' )
389359 if isinstance (part , str ):
390360 return types .Part (text = part )
391- if _is_duck_type_of (part , types .File ):
361+ if is_duck_type_of (part , types .File ):
392362 if not part .uri or not part .mime_type : # type: ignore[union-attr]
393363 raise ValueError ('file uri and mime_type are required.' )
394364 return types .Part .from_uri (file_uri = part .uri , mime_type = part .mime_type ) # type: ignore[union-attr]
@@ -397,7 +367,7 @@ def t_part(part: Optional[types.PartUnionDict]) -> types.Part:
397367 return types .Part .model_validate (part )
398368 except pydantic .ValidationError :
399369 return types .Part (file_data = types .FileData .model_validate (part ))
400- if _is_duck_type_of (part , types .Part ):
370+ if is_duck_type_of (part , types .Part ):
401371 return part # type: ignore[return-value]
402372
403373 if 'image' in part .__class__ .__name__ .lower ():
@@ -454,7 +424,7 @@ def t_content(
454424) -> types .Content :
455425 if content is None :
456426 raise ValueError ('content is required.' )
457- if _is_duck_type_of (content , types .Content ):
427+ if is_duck_type_of (content , types .Content ):
458428 return content # type: ignore[return-value]
459429 if isinstance (content , dict ):
460430 try :
@@ -466,9 +436,9 @@ def t_content(
466436 if possible_part .function_call
467437 else types .UserContent (parts = [possible_part ])
468438 )
469- if _is_duck_type_of (content , types .File ):
439+ if is_duck_type_of (content , types .File ):
470440 return types .UserContent (parts = [t_part (content )]) # type: ignore[arg-type]
471- if _is_duck_type_of (content , types .Part ):
441+ if is_duck_type_of (content , types .Part ):
472442 return (
473443 types .ModelContent (parts = [content ]) # type: ignore[arg-type]
474444 if content .function_call # type: ignore[union-attr]
@@ -521,8 +491,8 @@ def _is_part(
521491 ) -> TypeGuard [types .PartUnionDict ]:
522492 if (
523493 isinstance (part , str )
524- or _is_duck_type_of (part , types .File )
525- or _is_duck_type_of (part , types .Part )
494+ or is_duck_type_of (part , types .File )
495+ or is_duck_type_of (part , types .Part )
526496 ):
527497 return True
528498
@@ -592,7 +562,7 @@ def _handle_current_part(
592562 # append to result
593563 # if list, we only accept a list of types.PartUnion
594564 for content in contents :
595- if _is_duck_type_of (content , types .Content ) or isinstance (content , list ):
565+ if is_duck_type_of (content , types .Content ) or isinstance (content , list ):
596566 _append_accumulated_parts_as_content (result , accumulated_parts )
597567 if isinstance (content , list ):
598568 result .append (types .UserContent (parts = content )) # type: ignore[arg-type]
@@ -889,7 +859,7 @@ def t_schema(
889859 return types .Schema .model_validate (origin )
890860 if isinstance (origin , EnumMeta ):
891861 return _process_enum (origin , client )
892- if _is_duck_type_of (origin , types .Schema ):
862+ if is_duck_type_of (origin , types .Schema ):
893863 if dict (origin ) == dict (types .Schema ()): # type: ignore [arg-type]
894864 # response_schema value was coerced to an empty Schema instance because
895865 # it did not adhere to the Schema field annotation
@@ -931,7 +901,7 @@ def t_speech_config(
931901) -> Optional [types .SpeechConfig ]:
932902 if not origin :
933903 return None
934- if _is_duck_type_of (origin , types .SpeechConfig ):
904+ if is_duck_type_of (origin , types .SpeechConfig ):
935905 return origin # type: ignore[return-value]
936906 if isinstance (origin , str ):
937907 return types .SpeechConfig (
@@ -948,7 +918,7 @@ def t_speech_config(
948918def t_live_speech_config (
949919 origin : types .SpeechConfigOrDict ,
950920) -> Optional [types .SpeechConfig ]:
951- if _is_duck_type_of (origin , types .SpeechConfig ):
921+ if is_duck_type_of (origin , types .SpeechConfig ):
952922 speech_config = origin
953923 if isinstance (origin , dict ):
954924 speech_config = types .SpeechConfig .model_validate (origin )
@@ -974,7 +944,7 @@ def t_tool(
974944 )
975945 ]
976946 )
977- elif McpTool is not None and _is_duck_type_of (origin , McpTool ):
947+ elif McpTool is not None and is_duck_type_of (origin , McpTool ):
978948 return mcp_to_gemini_tool (origin )
979949 elif isinstance (origin , dict ):
980950 return types .Tool .model_validate (origin )
@@ -1017,7 +987,7 @@ def t_batch_job_source(
1017987) -> types .BatchJobSource :
1018988 if isinstance (src , dict ):
1019989 src = types .BatchJobSource (** src )
1020- if _is_duck_type_of (src , types .BatchJobSource ):
990+ if is_duck_type_of (src , types .BatchJobSource ):
1021991 vertex_sources = sum (
1022992 [src .gcs_uri is not None , src .bigquery_uri is not None ] # type: ignore[union-attr]
1023993 )
@@ -1068,7 +1038,7 @@ def t_embedding_batch_job_source(
10681038 if isinstance (src , dict ):
10691039 src = types .EmbeddingsBatchJobSource (** src )
10701040
1071- if _is_duck_type_of (src , types .EmbeddingsBatchJobSource ):
1041+ if is_duck_type_of (src , types .EmbeddingsBatchJobSource ):
10721042 mldev_sources = sum ([
10731043 src .inlined_requests is not None ,
10741044 src .file_name is not None ,
@@ -1103,7 +1073,7 @@ def t_batch_job_destination(
11031073 )
11041074 else :
11051075 raise ValueError (f'Unsupported destination: { dest } ' )
1106- elif _is_duck_type_of (dest , types .BatchJobDestination ):
1076+ elif is_duck_type_of (dest , types .BatchJobDestination ):
11071077 return dest
11081078 else :
11091079 raise ValueError (f'Unsupported destination: { dest } ' )
@@ -1203,11 +1173,11 @@ def t_file_name(
12031173 name : Optional [Union [str , types .File , types .Video , types .GeneratedVideo ]],
12041174) -> str :
12051175 # Remove the files/ prefix since it's added to the url path.
1206- if _is_duck_type_of (name , types .File ):
1176+ if is_duck_type_of (name , types .File ):
12071177 name = name .name # type: ignore[union-attr]
1208- elif _is_duck_type_of (name , types .Video ):
1178+ elif is_duck_type_of (name , types .Video ):
12091179 name = name .uri # type: ignore[union-attr]
1210- elif _is_duck_type_of (name , types .GeneratedVideo ):
1180+ elif is_duck_type_of (name , types .GeneratedVideo ):
12111181 if name .video is not None : # type: ignore[union-attr]
12121182 name = name .video .uri # type: ignore[union-attr]
12131183 else :
@@ -1252,7 +1222,7 @@ def t_tuning_job_status(status: str) -> Union[types.JobState, str]:
12521222def t_content_strict (content : types .ContentOrDict ) -> types .Content :
12531223 if isinstance (content , dict ):
12541224 return types .Content .model_validate (content )
1255- elif _is_duck_type_of (content , types .Content ):
1225+ elif is_duck_type_of (content , types .Content ):
12561226 return content
12571227 else :
12581228 raise ValueError (
0 commit comments