feat(phase1): add image storage utilities
- Create backend/services/image_storage.py with 4 core functions:
- sanitize_filename(): remove unsafe chars, limit to 255 chars, convert to lowercase
- get_unique_filename(): handle collisions with UUID suffix (format: {name}_{uuid8}_{variant}.jpg)
- ensure_image_directories(): create /images/ root and category subdirs on startup
- save_image(): save bytes to /images/{category}/{filename}, returns relative path
- Create comprehensive test suite (22 tests) covering all functionality
- Integrate ensure_image_directories() into FastAPI startup event
- Directory structure: /images/{category}/{filename}
- Collision handling: auto-suffix with UUID if filename exists
- All tests passing, pathlib.Path for safe operations
This commit is contained in:
26
venv/lib/python3.12/site-packages/google/genai/__init__.py
Normal file
26
venv/lib/python3.12/site-packages/google/genai/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""Google Gen AI SDK"""
|
||||
|
||||
from . import interactions
|
||||
from . import types
|
||||
from . import version
|
||||
from .client import Client
|
||||
|
||||
|
||||
__version__ = version.__version__
|
||||
|
||||
__all__ = ['Client']
|
||||
55
venv/lib/python3.12/site-packages/google/genai/_adapters.py
Normal file
55
venv/lib/python3.12/site-packages/google/genai/_adapters.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import typing
|
||||
|
||||
from ._mcp_utils import mcp_to_gemini_tools
|
||||
from .types import FunctionCall, Tool
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from mcp import types as mcp_types
|
||||
from mcp import ClientSession
|
||||
|
||||
|
||||
class McpToGenAiToolAdapter:
|
||||
"""Adapter for working with MCP tools in a GenAI client."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: "mcp.ClientSession", # type: ignore # noqa: F821
|
||||
list_tools_result: "mcp_types.ListToolsResult", # type: ignore
|
||||
) -> None:
|
||||
self._mcp_session = session
|
||||
self._list_tools_result = list_tools_result
|
||||
|
||||
async def call_tool(
|
||||
self, function_call: FunctionCall
|
||||
) -> "mcp_types.CallToolResult": # type: ignore
|
||||
"""Calls a function on the MCP server."""
|
||||
name = function_call.name if function_call.name else ""
|
||||
arguments = dict(function_call.args) if function_call.args else {}
|
||||
|
||||
return typing.cast(
|
||||
"mcp_types.CallToolResult",
|
||||
await self._mcp_session.call_tool(
|
||||
name=name,
|
||||
arguments=arguments,
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def tools(self) -> list[Tool]:
|
||||
"""Returns a list of Google GenAI tools."""
|
||||
return mcp_to_gemini_tools(self._list_tools_result.tools)
|
||||
2157
venv/lib/python3.12/site-packages/google/genai/_api_client.py
Normal file
2157
venv/lib/python3.12/site-packages/google/genai/_api_client.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""Utilities for the API Modules of the Google Gen AI SDK."""
|
||||
|
||||
from typing import Optional
|
||||
from . import _api_client
|
||||
|
||||
|
||||
class BaseModule:
|
||||
|
||||
def __init__(self, api_client_: _api_client.BaseApiClient):
|
||||
self._api_client = api_client_
|
||||
|
||||
@property
|
||||
def vertexai(self) -> Optional[bool]:
|
||||
return self._api_client.vertexai
|
||||
@@ -0,0 +1,325 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
import types as builtin_types
|
||||
import typing
|
||||
from typing import _GenericAlias, Any, Callable, get_args, get_origin, Literal, Optional, Union # type: ignore[attr-defined]
|
||||
|
||||
import pydantic
|
||||
|
||||
from . import _extra_utils
|
||||
from . import types
|
||||
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
VersionedUnionType = builtin_types.UnionType
|
||||
else:
|
||||
VersionedUnionType = typing._UnionGenericAlias # type: ignore[attr-defined]
|
||||
|
||||
|
||||
__all__ = [
|
||||
'_py_builtin_type_to_schema_type',
|
||||
'_raise_for_unsupported_param',
|
||||
'_handle_params_as_deferred_annotations',
|
||||
'_add_unevaluated_items_to_fixed_len_tuple_schema',
|
||||
'_is_builtin_primitive_or_compound',
|
||||
'_is_default_value_compatible',
|
||||
'_parse_schema_from_parameter',
|
||||
'_get_required_fields',
|
||||
]
|
||||
|
||||
_py_builtin_type_to_schema_type = {
|
||||
str: types.Type.STRING,
|
||||
int: types.Type.INTEGER,
|
||||
float: types.Type.NUMBER,
|
||||
bool: types.Type.BOOLEAN,
|
||||
list: types.Type.ARRAY,
|
||||
dict: types.Type.OBJECT,
|
||||
None: types.Type.NULL,
|
||||
}
|
||||
|
||||
|
||||
def _raise_for_unsupported_param(
|
||||
param: inspect.Parameter, func_name: str, exception: Union[Exception, type[Exception]]
|
||||
) -> None:
|
||||
raise ValueError(
|
||||
f'Failed to parse the parameter {param} of function {func_name} for'
|
||||
' automatic function calling.Automatic function calling works best with'
|
||||
' simpler function signature schema, consider manually parsing your'
|
||||
f' function declaration for function {func_name}.'
|
||||
) from exception
|
||||
|
||||
|
||||
def _handle_params_as_deferred_annotations(param: inspect.Parameter, annotation_under_future: dict[str, Any], name: str) -> inspect.Parameter:
|
||||
"""Catches the case when type hints are stored as strings."""
|
||||
if isinstance(param.annotation, str):
|
||||
param = param.replace(annotation=annotation_under_future[name])
|
||||
return param
|
||||
|
||||
|
||||
def _add_unevaluated_items_to_fixed_len_tuple_schema(
|
||||
json_schema: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
if (
|
||||
json_schema.get('maxItems')
|
||||
and (
|
||||
json_schema.get('prefixItems')
|
||||
and len(json_schema['prefixItems']) == json_schema['maxItems']
|
||||
)
|
||||
and json_schema.get('type') == 'array'
|
||||
):
|
||||
json_schema['unevaluatedItems'] = False
|
||||
return json_schema
|
||||
|
||||
|
||||
def _is_builtin_primitive_or_compound(
|
||||
annotation: inspect.Parameter.annotation, # type: ignore[valid-type]
|
||||
) -> bool:
|
||||
return annotation in _py_builtin_type_to_schema_type.keys()
|
||||
|
||||
|
||||
def _is_default_value_compatible(
|
||||
default_value: Any, annotation: inspect.Parameter.annotation # type: ignore[valid-type]
|
||||
) -> bool:
|
||||
# None type is expected to be handled external to this function
|
||||
if _is_builtin_primitive_or_compound(annotation):
|
||||
return isinstance(default_value, annotation)
|
||||
|
||||
if (
|
||||
isinstance(annotation, _GenericAlias)
|
||||
or isinstance(annotation, builtin_types.GenericAlias)
|
||||
or isinstance(annotation, VersionedUnionType)
|
||||
):
|
||||
origin = get_origin(annotation)
|
||||
if origin in (Union, VersionedUnionType): # type: ignore[comparison-overlap]
|
||||
return any(
|
||||
_is_default_value_compatible(default_value, arg)
|
||||
for arg in get_args(annotation)
|
||||
)
|
||||
|
||||
if origin is dict: # type: ignore[comparison-overlap]
|
||||
return isinstance(default_value, dict)
|
||||
|
||||
if origin is list: # type: ignore[comparison-overlap]
|
||||
if not isinstance(default_value, list):
|
||||
return False
|
||||
# most tricky case, element in list is union type
|
||||
# need to apply any logic within all
|
||||
# see test case test_generic_alias_complex_array_with_default_value
|
||||
# a: typing.List[int | str | float | bool]
|
||||
# default_value: [1, 'a', 1.1, True]
|
||||
return all(
|
||||
any(
|
||||
_is_default_value_compatible(item, arg)
|
||||
for arg in get_args(annotation)
|
||||
)
|
||||
for item in default_value
|
||||
)
|
||||
|
||||
if origin is Literal: # type: ignore[comparison-overlap]
|
||||
return default_value in get_args(annotation)
|
||||
|
||||
# return False for any other unrecognized annotation
|
||||
return False
|
||||
|
||||
|
||||
def _parse_schema_from_parameter( # type: ignore[return]
|
||||
api_option: Literal['VERTEX_AI', 'GEMINI_API'],
|
||||
param: inspect.Parameter,
|
||||
func_name: str,
|
||||
) -> types.Schema:
|
||||
"""parse schema from parameter.
|
||||
|
||||
from the simplest case to the most complex case.
|
||||
"""
|
||||
schema = types.Schema()
|
||||
default_value_error_msg = (
|
||||
f'Default value {param.default} of parameter {param} of function'
|
||||
f' {func_name} is not compatible with the parameter annotation'
|
||||
f' {param.annotation}.'
|
||||
)
|
||||
if _is_builtin_primitive_or_compound(param.annotation):
|
||||
if param.default is not inspect.Parameter.empty:
|
||||
if not _is_default_value_compatible(param.default, param.annotation):
|
||||
raise ValueError(default_value_error_msg)
|
||||
schema.default = param.default
|
||||
schema.type = _py_builtin_type_to_schema_type[param.annotation]
|
||||
return schema
|
||||
if (
|
||||
isinstance(param.annotation, VersionedUnionType)
|
||||
# only parse simple UnionType, example int | str | float | bool
|
||||
# complex UnionType will be invoked in raise branch
|
||||
and all(
|
||||
(_is_builtin_primitive_or_compound(arg) or arg is type(None))
|
||||
for arg in get_args(param.annotation)
|
||||
)
|
||||
):
|
||||
schema.type = _py_builtin_type_to_schema_type[dict]
|
||||
schema.any_of = []
|
||||
unique_types = set()
|
||||
for arg in get_args(param.annotation):
|
||||
if arg.__name__ == 'NoneType': # Optional type
|
||||
schema.nullable = True
|
||||
continue
|
||||
schema_in_any_of = _parse_schema_from_parameter(
|
||||
api_option,
|
||||
inspect.Parameter(
|
||||
'item', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=arg
|
||||
),
|
||||
func_name,
|
||||
)
|
||||
if (
|
||||
schema_in_any_of.model_dump_json(exclude_none=True)
|
||||
not in unique_types
|
||||
):
|
||||
schema.any_of.append(schema_in_any_of)
|
||||
unique_types.add(schema_in_any_of.model_dump_json(exclude_none=True))
|
||||
if len(schema.any_of) == 1: # param: list | None -> Array
|
||||
schema.type = schema.any_of[0].type
|
||||
schema.any_of = None
|
||||
if (
|
||||
param.default is not inspect.Parameter.empty
|
||||
and param.default is not None
|
||||
):
|
||||
if not _is_default_value_compatible(param.default, param.annotation):
|
||||
raise ValueError(default_value_error_msg)
|
||||
schema.default = param.default
|
||||
return schema
|
||||
if isinstance(param.annotation, _GenericAlias) or isinstance(
|
||||
param.annotation, builtin_types.GenericAlias
|
||||
):
|
||||
origin = get_origin(param.annotation)
|
||||
args = get_args(param.annotation)
|
||||
if origin is dict:
|
||||
schema.type = _py_builtin_type_to_schema_type[dict]
|
||||
if param.default is not inspect.Parameter.empty:
|
||||
if not _is_default_value_compatible(param.default, param.annotation):
|
||||
raise ValueError(default_value_error_msg)
|
||||
schema.default = param.default
|
||||
return schema
|
||||
if origin is Literal:
|
||||
if not all(isinstance(arg, str) for arg in args):
|
||||
raise ValueError(
|
||||
f'Literal type {param.annotation} must be a list of strings.'
|
||||
)
|
||||
schema.type = _py_builtin_type_to_schema_type[str]
|
||||
schema.enum = list(args)
|
||||
if param.default is not inspect.Parameter.empty:
|
||||
if not _is_default_value_compatible(param.default, param.annotation):
|
||||
raise ValueError(default_value_error_msg)
|
||||
schema.default = param.default
|
||||
return schema
|
||||
if origin is list:
|
||||
schema.type = _py_builtin_type_to_schema_type[list]
|
||||
schema.items = _parse_schema_from_parameter(
|
||||
api_option,
|
||||
inspect.Parameter(
|
||||
'item',
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
annotation=args[0],
|
||||
),
|
||||
func_name,
|
||||
)
|
||||
if param.default is not inspect.Parameter.empty:
|
||||
if not _is_default_value_compatible(param.default, param.annotation):
|
||||
raise ValueError(default_value_error_msg)
|
||||
schema.default = param.default
|
||||
return schema
|
||||
if origin is Union:
|
||||
schema.any_of = []
|
||||
schema.type = _py_builtin_type_to_schema_type[dict]
|
||||
unique_types = set()
|
||||
for arg in args:
|
||||
# The first check is for NoneType in Python 3.9, since the __name__
|
||||
# attribute is not available in Python 3.9
|
||||
if type(arg) is type(None) or (
|
||||
hasattr(arg, '__name__') and arg.__name__ == 'NoneType'
|
||||
): # Optional type
|
||||
schema.nullable = True
|
||||
continue
|
||||
schema_in_any_of = _parse_schema_from_parameter(
|
||||
api_option,
|
||||
inspect.Parameter(
|
||||
'item',
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
annotation=arg,
|
||||
),
|
||||
func_name,
|
||||
)
|
||||
if (
|
||||
len(param.annotation.__args__) == 2
|
||||
and type(None) in param.annotation.__args__
|
||||
): # Optional type
|
||||
for optional_arg in param.annotation.__args__:
|
||||
if (
|
||||
hasattr(optional_arg, '__origin__')
|
||||
and optional_arg.__origin__ is list
|
||||
):
|
||||
# Optional type with list, for example Optional[list[str]]
|
||||
schema.items = schema_in_any_of.items
|
||||
if (
|
||||
schema_in_any_of.model_dump_json(exclude_none=True)
|
||||
not in unique_types
|
||||
):
|
||||
schema.any_of.append(schema_in_any_of)
|
||||
unique_types.add(schema_in_any_of.model_dump_json(exclude_none=True))
|
||||
if len(schema.any_of) == 1: # param: Union[List, None] -> Array
|
||||
schema.type = schema.any_of[0].type
|
||||
schema.any_of = None
|
||||
if (
|
||||
param.default is not None
|
||||
and param.default is not inspect.Parameter.empty
|
||||
):
|
||||
if not _is_default_value_compatible(param.default, param.annotation):
|
||||
raise ValueError(default_value_error_msg)
|
||||
schema.default = param.default
|
||||
return schema
|
||||
# all other generic alias will be invoked in raise branch
|
||||
if (
|
||||
# for user defined class, we only support pydantic model
|
||||
_extra_utils.is_annotation_pydantic_model(param.annotation)
|
||||
):
|
||||
if (
|
||||
param.default is not inspect.Parameter.empty
|
||||
and param.default is not None
|
||||
):
|
||||
schema.default = param.default
|
||||
schema.type = _py_builtin_type_to_schema_type[dict]
|
||||
schema.properties = {}
|
||||
for field_name, field_info in param.annotation.model_fields.items():
|
||||
schema.properties[field_name] = _parse_schema_from_parameter(
|
||||
api_option,
|
||||
inspect.Parameter(
|
||||
field_name,
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
annotation=field_info.annotation,
|
||||
),
|
||||
func_name,
|
||||
)
|
||||
schema.required = _get_required_fields(schema)
|
||||
return schema
|
||||
_raise_for_unsupported_param(param, func_name, ValueError)
|
||||
|
||||
|
||||
def _get_required_fields(schema: types.Schema) -> Optional[list[str]]:
|
||||
if not schema.properties:
|
||||
return None
|
||||
return [
|
||||
field_name
|
||||
for field_name, field_schema in schema.properties.items()
|
||||
if not field_schema.nullable and field_schema.default is None
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""Base transformers for Google GenAI SDK."""
|
||||
import base64
|
||||
|
||||
# Some fields don't accept url safe base64 encoding.
|
||||
# We shouldn't use this transformer if the backend adhere to Cloud Type
|
||||
# format https://cloud.google.com/docs/discovery/type-format.
|
||||
# TODO(b/389133914,b/390320301): Remove the hack after backend fix the issue.
|
||||
def t_bytes(data: bytes) -> str:
|
||||
if not isinstance(data, bytes):
|
||||
return data
|
||||
return base64.b64encode(data).decode('ascii')
|
||||
50
venv/lib/python3.12/site-packages/google/genai/_base_url.py
Normal file
50
venv/lib/python3.12/site-packages/google/genai/_base_url.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from .types import HttpOptions
|
||||
|
||||
_default_base_gemini_url = None
|
||||
_default_base_vertex_url = None
|
||||
|
||||
|
||||
def set_default_base_urls(
|
||||
gemini_url: Optional[str], vertex_url: Optional[str]
|
||||
) -> None:
|
||||
"""Overrides the base URLs for the Gemini API and Vertex AI API."""
|
||||
global _default_base_gemini_url, _default_base_vertex_url
|
||||
_default_base_gemini_url = gemini_url
|
||||
_default_base_vertex_url = vertex_url
|
||||
|
||||
|
||||
def get_base_url(
|
||||
vertexai: bool,
|
||||
http_options: Optional[HttpOptions] = None,
|
||||
) -> Optional[str]:
|
||||
"""Returns the default base URL based on the following priority.
|
||||
|
||||
1. Base URLs set via HttpOptions.
|
||||
2. Base URLs set via the latest call to setDefaultBaseUrls.
|
||||
3. Base URLs set via environment variables.
|
||||
"""
|
||||
if http_options and http_options.base_url:
|
||||
return http_options.base_url
|
||||
|
||||
if vertexai:
|
||||
return _default_base_vertex_url or os.getenv('GOOGLE_VERTEX_BASE_URL')
|
||||
else:
|
||||
return _default_base_gemini_url or os.getenv('GOOGLE_GEMINI_BASE_URL')
|
||||
847
venv/lib/python3.12/site-packages/google/genai/_common.py
Normal file
847
venv/lib/python3.12/site-packages/google/genai/_common.py
Normal file
@@ -0,0 +1,847 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""Common utilities for the SDK."""
|
||||
|
||||
import base64
|
||||
import collections.abc
|
||||
import datetime
|
||||
import enum
|
||||
import functools
|
||||
import logging
|
||||
import re
|
||||
import typing
|
||||
from typing import Any, Callable, FrozenSet, Optional, Union, get_args, get_origin
|
||||
import uuid
|
||||
import warnings
|
||||
import pydantic
|
||||
from pydantic import alias_generators
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
logger = logging.getLogger('google_genai._common')
|
||||
|
||||
StringDict: TypeAlias = dict[str, Any]
|
||||
|
||||
|
||||
class ExperimentalWarning(Warning):
|
||||
"""Warning for experimental features."""
|
||||
|
||||
|
||||
def set_value_by_path(
|
||||
data: Optional[dict[Any, Any]], keys: list[str], value: Any
|
||||
) -> None:
|
||||
"""Examples:
|
||||
|
||||
set_value_by_path({}, ['a', 'b'], v)
|
||||
-> {'a': {'b': v}}
|
||||
set_value_by_path({}, ['a', 'b[]', c], [v1, v2])
|
||||
-> {'a': {'b': [{'c': v1}, {'c': v2}]}}
|
||||
set_value_by_path({'a': {'b': [{'c': v1}, {'c': v2}]}}, ['a', 'b[]', 'd'], v3)
|
||||
-> {'a': {'b': [{'c': v1, 'd': v3}, {'c': v2, 'd': v3}]}}
|
||||
"""
|
||||
if value is None:
|
||||
return
|
||||
for i, key in enumerate(keys[:-1]):
|
||||
if key.endswith('[]'):
|
||||
key_name = key[:-2]
|
||||
if data is not None and key_name not in data:
|
||||
if isinstance(value, list):
|
||||
data[key_name] = [{} for _ in range(len(value))]
|
||||
else:
|
||||
raise ValueError(
|
||||
f'value {value} must be a list given an array path {key}'
|
||||
)
|
||||
if isinstance(value, list) and data is not None:
|
||||
for j, d in enumerate(data[key_name]):
|
||||
set_value_by_path(d, keys[i + 1 :], value[j])
|
||||
else:
|
||||
if data is not None:
|
||||
for d in data[key_name]:
|
||||
set_value_by_path(d, keys[i + 1 :], value)
|
||||
return
|
||||
elif key.endswith('[0]'):
|
||||
key_name = key[:-3]
|
||||
if data is not None and key_name not in data:
|
||||
data[key_name] = [{}]
|
||||
if data is not None:
|
||||
set_value_by_path(data[key_name][0], keys[i + 1 :], value)
|
||||
return
|
||||
if data is not None:
|
||||
data = data.setdefault(key, {})
|
||||
|
||||
if data is not None:
|
||||
existing_data = data.get(keys[-1])
|
||||
# If there is an existing value, merge, not overwrite.
|
||||
if existing_data is not None:
|
||||
# Don't overwrite existing non-empty value with new empty value.
|
||||
# This is triggered when handling tuning datasets.
|
||||
if not value:
|
||||
pass
|
||||
# Don't fail when overwriting value with same value
|
||||
elif value == existing_data:
|
||||
pass
|
||||
# Instead of overwriting dictionary with another dictionary, merge them.
|
||||
# This is important for handling training and validation datasets in tuning.
|
||||
elif isinstance(existing_data, dict) and isinstance(value, dict):
|
||||
# Merging dictionaries. Consider deep merging in the future.
|
||||
existing_data.update(value)
|
||||
else:
|
||||
raise ValueError(
|
||||
f'Cannot set value for an existing key. Key: {keys[-1]};'
|
||||
f' Existing value: {existing_data}; New value: {value}.'
|
||||
)
|
||||
else:
|
||||
if (
|
||||
keys[-1] == '_self'
|
||||
and isinstance(data, dict)
|
||||
and isinstance(value, dict)
|
||||
):
|
||||
data.update(value)
|
||||
else:
|
||||
data[keys[-1]] = value
|
||||
|
||||
|
||||
def get_value_by_path(
|
||||
data: Any, keys: list[str], *, default_value: Any = None
|
||||
) -> Any:
|
||||
"""Examples:
|
||||
|
||||
get_value_by_path({'a': {'b': v}}, ['a', 'b'])
|
||||
-> v
|
||||
get_value_by_path({'a': {'b': [{'c': v1}, {'c': v2}]}}, ['a', 'b[]', 'c'])
|
||||
-> [v1, v2]
|
||||
"""
|
||||
if keys == ['_self']:
|
||||
return data
|
||||
for i, key in enumerate(keys):
|
||||
if not data:
|
||||
return default_value
|
||||
if key.endswith('[]'):
|
||||
key_name = key[:-2]
|
||||
if key_name in data:
|
||||
return [
|
||||
get_value_by_path(d, keys[i + 1 :], default_value=default_value)
|
||||
for d in data[key_name]
|
||||
]
|
||||
else:
|
||||
return default_value
|
||||
elif key.endswith('[0]'):
|
||||
key_name = key[:-3]
|
||||
if key_name in data and data[key_name]:
|
||||
return get_value_by_path(
|
||||
data[key_name][0], keys[i + 1 :], default_value=default_value
|
||||
)
|
||||
else:
|
||||
return default_value
|
||||
else:
|
||||
if key in data:
|
||||
data = data[key]
|
||||
elif isinstance(data, BaseModel) and hasattr(data, key):
|
||||
data = getattr(data, key)
|
||||
else:
|
||||
return default_value
|
||||
return data
|
||||
|
||||
|
||||
def move_value_by_path(data: Any, paths: dict[str, str]) -> None:
|
||||
"""Moves values from source paths to destination paths.
|
||||
|
||||
Examples:
|
||||
move_value_by_path(
|
||||
{'requests': [{'content': v1}, {'content': v2}]},
|
||||
{'requests[].*': 'requests[].request.*'}
|
||||
)
|
||||
-> {'requests': [{'request': {'content': v1}}, {'request': {'content':
|
||||
v2}}]}
|
||||
"""
|
||||
for source_path, dest_path in paths.items():
|
||||
source_keys = source_path.split('.')
|
||||
dest_keys = dest_path.split('.')
|
||||
|
||||
# Determine keys to exclude from wildcard to avoid cyclic references
|
||||
exclude_keys = set()
|
||||
wildcard_idx = -1
|
||||
for i, key in enumerate(source_keys):
|
||||
if key == '*':
|
||||
wildcard_idx = i
|
||||
break
|
||||
|
||||
if wildcard_idx != -1 and len(dest_keys) > wildcard_idx:
|
||||
# Extract the intermediate key between source and dest paths
|
||||
# Example: source=['requests[]', '*'], dest=['requests[]', 'request', '*']
|
||||
# We want to exclude 'request'
|
||||
for i in range(wildcard_idx, len(dest_keys)):
|
||||
key = dest_keys[i]
|
||||
if key != '*' and not key.endswith('[]') and not key.endswith('[0]'):
|
||||
exclude_keys.add(key)
|
||||
|
||||
# Move values recursively
|
||||
_move_value_recursive(data, source_keys, dest_keys, 0, exclude_keys)
|
||||
|
||||
|
||||
def _move_value_recursive(
|
||||
data: Any,
|
||||
source_keys: list[str],
|
||||
dest_keys: list[str],
|
||||
key_idx: int,
|
||||
exclude_keys: set[str],
|
||||
) -> None:
|
||||
"""Recursively moves values from source path to destination path."""
|
||||
if key_idx >= len(source_keys):
|
||||
return
|
||||
|
||||
key = source_keys[key_idx]
|
||||
|
||||
if key.endswith('[]'):
|
||||
# Handle array iteration
|
||||
key_name = key[:-2]
|
||||
if key_name in data and isinstance(data[key_name], list):
|
||||
for item in data[key_name]:
|
||||
_move_value_recursive(
|
||||
item, source_keys, dest_keys, key_idx + 1, exclude_keys
|
||||
)
|
||||
elif key == '*':
|
||||
# Handle wildcard - move all fields
|
||||
if isinstance(data, dict):
|
||||
# Get all keys to move (excluding specified keys)
|
||||
keys_to_move = [
|
||||
k
|
||||
for k in list(data.keys())
|
||||
if not k.startswith('_') and k not in exclude_keys
|
||||
]
|
||||
|
||||
# Collect values to move
|
||||
values_to_move = {k: data[k] for k in keys_to_move}
|
||||
|
||||
# Set values at destination
|
||||
for k, v in values_to_move.items():
|
||||
# Build destination keys with the field name
|
||||
new_dest_keys = []
|
||||
for dk in dest_keys[key_idx:]:
|
||||
if dk == '*':
|
||||
new_dest_keys.append(k)
|
||||
else:
|
||||
new_dest_keys.append(dk)
|
||||
set_value_by_path(data, new_dest_keys, v)
|
||||
|
||||
# Delete from source
|
||||
for k in keys_to_move:
|
||||
del data[k]
|
||||
else:
|
||||
# Navigate to next level
|
||||
if key in data:
|
||||
_move_value_recursive(
|
||||
data[key], source_keys, dest_keys, key_idx + 1, exclude_keys
|
||||
)
|
||||
|
||||
|
||||
def maybe_snake_to_camel(snake_str: str, convert: bool = True) -> str:
|
||||
"""Converts a snake_case string to CamelCase, if convert is True."""
|
||||
if not convert:
|
||||
return snake_str
|
||||
return re.sub(r'_([a-zA-Z])', lambda match: match.group(1).upper(), snake_str)
|
||||
|
||||
|
||||
def convert_to_dict(obj: object, convert_keys: bool = False) -> Any:
|
||||
"""Recursively converts a given object to a dictionary.
|
||||
|
||||
If the object is a Pydantic model, it uses the model's `model_dump()` method.
|
||||
|
||||
Args:
|
||||
obj: The object to convert.
|
||||
convert_keys: Whether to convert the keys from snake case to camel case.
|
||||
|
||||
Returns:
|
||||
A dictionary representation of the object, a list of objects if a list is
|
||||
passed, or the object itself if it is not a dictionary, list, or Pydantic
|
||||
model.
|
||||
"""
|
||||
if isinstance(obj, pydantic.BaseModel):
|
||||
return convert_to_dict(obj.model_dump(exclude_none=True), convert_keys)
|
||||
elif isinstance(obj, dict):
|
||||
return {
|
||||
maybe_snake_to_camel(key, convert_keys): convert_to_dict(value)
|
||||
for key, value in obj.items()
|
||||
}
|
||||
elif isinstance(obj, list):
|
||||
return [convert_to_dict(item, convert_keys) for item in obj]
|
||||
else:
|
||||
return obj
|
||||
|
||||
|
||||
def _is_struct_type(annotation: type) -> bool:
|
||||
"""Checks if the given annotation is list[dict[str, typing.Any]]
|
||||
|
||||
or typing.List[typing.Dict[str, typing.Any]].
|
||||
|
||||
This maps to Struct type in the API.
|
||||
"""
|
||||
outer_origin = get_origin(annotation)
|
||||
outer_args = get_args(annotation)
|
||||
|
||||
if outer_origin is not list: # Python 3.9+ normalizes list
|
||||
return False
|
||||
|
||||
if not outer_args or len(outer_args) != 1:
|
||||
return False
|
||||
|
||||
inner_annotation = outer_args[0]
|
||||
|
||||
inner_origin = get_origin(inner_annotation)
|
||||
inner_args = get_args(inner_annotation)
|
||||
|
||||
if inner_origin is not dict: # Python 3.9+ normalizes to dict
|
||||
return False
|
||||
|
||||
if not inner_args or len(inner_args) != 2:
|
||||
# dict should have exactly two type arguments
|
||||
return False
|
||||
|
||||
# Check if the dict arguments are str and typing.Any
|
||||
key_type, value_type = inner_args
|
||||
return key_type is str and value_type is typing.Any
|
||||
|
||||
|
||||
def _remove_extra_fields(model: Any, response: dict[str, object]) -> None:
|
||||
"""Removes extra fields from the response that are not in the model.
|
||||
|
||||
Mutates the response in place.
|
||||
"""
|
||||
|
||||
key_values = list(response.items())
|
||||
|
||||
for key, value in key_values:
|
||||
# Need to convert to snake case to match model fields names
|
||||
# ex: UsageMetadata
|
||||
alias_map = {
|
||||
field_info.alias: key for key, field_info in model.model_fields.items()
|
||||
}
|
||||
|
||||
if key not in model.model_fields and key not in alias_map:
|
||||
response.pop(key)
|
||||
continue
|
||||
|
||||
key = alias_map.get(key, key)
|
||||
|
||||
annotation = model.model_fields[key].annotation
|
||||
|
||||
# Get the BaseModel if Optional
|
||||
if typing.get_origin(annotation) is Union:
|
||||
annotation = typing.get_args(annotation)[0]
|
||||
|
||||
# if dict, assume BaseModel but also check that field type is not dict
|
||||
# example: FunctionCall.args
|
||||
if isinstance(value, dict) and typing.get_origin(annotation) is not dict:
|
||||
_remove_extra_fields(annotation, value)
|
||||
elif isinstance(value, list):
|
||||
if _is_struct_type(annotation):
|
||||
continue
|
||||
|
||||
for item in value:
|
||||
# assume a list of dict is list of BaseModel
|
||||
if isinstance(item, dict):
|
||||
_remove_extra_fields(typing.get_args(annotation)[0], item)
|
||||
|
||||
|
||||
T = typing.TypeVar('T', bound='BaseModel')
|
||||
|
||||
|
||||
def _pretty_repr(
|
||||
obj: Any,
|
||||
*,
|
||||
indent_level: int = 0,
|
||||
indent_delta: int = 2,
|
||||
max_len: int = 100,
|
||||
max_items: int = 5,
|
||||
depth: int = 6,
|
||||
visited: Optional[FrozenSet[int]] = None,
|
||||
) -> str:
|
||||
"""Returns a representation of the given object."""
|
||||
if visited is None:
|
||||
visited = frozenset()
|
||||
|
||||
obj_id = id(obj)
|
||||
if obj_id in visited:
|
||||
return '<... Circular reference ...>'
|
||||
|
||||
if depth < 0:
|
||||
return '<... Max depth ...>'
|
||||
|
||||
visited = frozenset(list(visited) + [obj_id])
|
||||
|
||||
indent = ' ' * indent_level
|
||||
next_indent_str = ' ' * (indent_level + indent_delta)
|
||||
|
||||
if isinstance(obj, pydantic.BaseModel):
|
||||
cls_name = obj.__class__.__name__
|
||||
items = []
|
||||
# Sort fields for consistent output
|
||||
fields = sorted(type(obj).model_fields)
|
||||
|
||||
for field_name in fields:
|
||||
field_info = type(obj).model_fields[field_name]
|
||||
if not field_info.repr: # Respect Field(repr=False)
|
||||
continue
|
||||
|
||||
try:
|
||||
value = getattr(obj, field_name)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
value_repr = _pretty_repr(
|
||||
value,
|
||||
indent_level=indent_level + indent_delta,
|
||||
indent_delta=indent_delta,
|
||||
max_len=max_len,
|
||||
max_items=max_items,
|
||||
depth=depth - 1,
|
||||
visited=visited,
|
||||
)
|
||||
items.append(f'{next_indent_str}{field_name}={value_repr}')
|
||||
|
||||
if not items:
|
||||
return f'{cls_name}()'
|
||||
return f'{cls_name}(\n' + ',\n'.join(items) + f'\n{indent})'
|
||||
elif isinstance(obj, str):
|
||||
if '\n' in obj:
|
||||
escaped = obj.replace('"""', '\\"\\"\\"')
|
||||
# Indent the multi-line string block contents
|
||||
return f'"""{escaped}"""'
|
||||
return repr(obj)
|
||||
elif isinstance(obj, bytes):
|
||||
if len(obj) > max_len:
|
||||
return f"{repr(obj[:max_len-3])[:-1]}...'"
|
||||
return repr(obj)
|
||||
elif isinstance(obj, collections.abc.Mapping):
|
||||
if not obj:
|
||||
return '{}'
|
||||
|
||||
# Check if the next level of recursion for keys/values will exceed the depth limit.
|
||||
if depth <= 0:
|
||||
item_count_str = f"{len(obj)} item{'s' if len(obj) != 1 else ''}"
|
||||
return f'{{<... {item_count_str} at Max depth ...>}}'
|
||||
|
||||
if len(obj) > max_items:
|
||||
return f'<dict len={len(obj)}>'
|
||||
|
||||
items = []
|
||||
try:
|
||||
sorted_keys = sorted(obj.keys(), key=str)
|
||||
except TypeError:
|
||||
sorted_keys = list(obj.keys())
|
||||
|
||||
for k in sorted_keys:
|
||||
v = obj[k]
|
||||
k_repr = _pretty_repr(
|
||||
k,
|
||||
indent_level=indent_level + indent_delta,
|
||||
indent_delta=indent_delta,
|
||||
max_len=max_len,
|
||||
max_items=max_items,
|
||||
depth=depth - 1,
|
||||
visited=visited,
|
||||
)
|
||||
v_repr = _pretty_repr(
|
||||
v,
|
||||
indent_level=indent_level + indent_delta,
|
||||
indent_delta=indent_delta,
|
||||
max_len=max_len,
|
||||
max_items=max_items,
|
||||
depth=depth - 1,
|
||||
visited=visited,
|
||||
)
|
||||
items.append(f'{next_indent_str}{k_repr}: {v_repr}')
|
||||
return f'{{\n' + ',\n'.join(items) + f'\n{indent}}}'
|
||||
elif isinstance(obj, (list, tuple, set)):
|
||||
return _format_collection(
|
||||
obj,
|
||||
indent_level=indent_level,
|
||||
indent_delta=indent_delta,
|
||||
max_len=max_len,
|
||||
max_items=max_items,
|
||||
depth=depth,
|
||||
visited=visited,
|
||||
)
|
||||
else:
|
||||
# Fallback to standard repr, indenting subsequent lines only
|
||||
raw_repr = repr(obj)
|
||||
# Replace newlines with newline + indent
|
||||
return raw_repr.replace('\n', f'\n{next_indent_str}')
|
||||
|
||||
|
||||
def _format_collection(
|
||||
obj: Any,
|
||||
*,
|
||||
indent_level: int,
|
||||
indent_delta: int,
|
||||
max_len: int,
|
||||
max_items: int,
|
||||
depth: int,
|
||||
visited: FrozenSet[int],
|
||||
) -> str:
|
||||
"""Formats a collection (list, tuple, set)."""
|
||||
if isinstance(obj, list):
|
||||
brackets = ('[', ']')
|
||||
internal_obj = obj
|
||||
elif isinstance(obj, tuple):
|
||||
brackets = ('(', ')')
|
||||
internal_obj = list(obj)
|
||||
elif isinstance(obj, set):
|
||||
internal_obj = list(obj)
|
||||
if obj:
|
||||
brackets = ('{', '}')
|
||||
else:
|
||||
brackets = ('set(', ')')
|
||||
else:
|
||||
raise ValueError(f'Unsupported collection type: {type(obj)}')
|
||||
|
||||
if not internal_obj:
|
||||
return brackets[0] + brackets[1]
|
||||
|
||||
# If the call to _pretty_repr for elements will have depth < 0
|
||||
if depth <= 0:
|
||||
item_count_str = f"{len(internal_obj)} item{'s'*(len(internal_obj)!=1)}"
|
||||
return f'{brackets[0]}<... {item_count_str} at Max depth ...>{brackets[1]}'
|
||||
|
||||
indent = ' ' * indent_level
|
||||
next_indent_str = ' ' * (indent_level + indent_delta)
|
||||
elements = []
|
||||
num_to_show = min(len(internal_obj), max_items)
|
||||
|
||||
for i in range(num_to_show):
|
||||
elem = internal_obj[i]
|
||||
elements.append(
|
||||
next_indent_str
|
||||
+ _pretty_repr(
|
||||
elem,
|
||||
indent_level=indent_level + indent_delta,
|
||||
indent_delta=indent_delta,
|
||||
max_len=max_len,
|
||||
max_items=max_items,
|
||||
depth=depth - 1,
|
||||
visited=visited,
|
||||
)
|
||||
)
|
||||
|
||||
if len(internal_obj) > max_items:
|
||||
elements.append(
|
||||
f'{next_indent_str}<... {len(internal_obj) - max_items} more items ...>'
|
||||
)
|
||||
|
||||
return f'{brackets[0]}\n' + ',\n'.join(elements) + f',\n{indent}{brackets[1]}'
|
||||
|
||||
|
||||
class BaseModel(pydantic.BaseModel):
|
||||
|
||||
model_config = pydantic.ConfigDict(
|
||||
alias_generator=alias_generators.to_camel,
|
||||
populate_by_name=True,
|
||||
from_attributes=True,
|
||||
protected_namespaces=(),
|
||||
extra='forbid',
|
||||
# This allows us to use arbitrary types in the model. E.g. PIL.Image.
|
||||
arbitrary_types_allowed=True,
|
||||
ser_json_bytes='base64',
|
||||
val_json_bytes='base64',
|
||||
ignored_types=(typing.TypeVar,),
|
||||
)
|
||||
|
||||
@pydantic.model_validator(mode='before')
|
||||
@classmethod
|
||||
def _check_field_type_mismatches(cls, data: Any) -> Any:
|
||||
"""Check for type mismatches and warn before Pydantic processes the data."""
|
||||
# Handle both dict and Pydantic model inputs
|
||||
if not isinstance(data, (dict, pydantic.BaseModel)):
|
||||
return data
|
||||
|
||||
for field_name, field_info in cls.model_fields.items():
|
||||
if isinstance(data, dict):
|
||||
value = data.get(field_name)
|
||||
else:
|
||||
value = getattr(data, field_name, None)
|
||||
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
expected_type = field_info.annotation
|
||||
origin = get_origin(expected_type)
|
||||
|
||||
if origin is Union:
|
||||
args = get_args(expected_type)
|
||||
non_none_types = [arg for arg in args if arg is not type(None)]
|
||||
if len(non_none_types) == 1:
|
||||
expected_type = non_none_types[0]
|
||||
|
||||
if (isinstance(expected_type, type) and
|
||||
get_origin(expected_type) is None and
|
||||
issubclass(expected_type, pydantic.BaseModel) and
|
||||
isinstance(value, pydantic.BaseModel) and
|
||||
not isinstance(value, expected_type)):
|
||||
logger.warning(
|
||||
f"Type mismatch in {cls.__name__}.{field_name}: "
|
||||
f"expected {expected_type.__name__}, got {type(value).__name__}"
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def __repr__(self) -> str:
|
||||
try:
|
||||
return _pretty_repr(self)
|
||||
except Exception:
|
||||
return super().__repr__()
|
||||
|
||||
@classmethod
|
||||
def _from_response(
|
||||
cls: typing.Type[T],
|
||||
*,
|
||||
response: dict[str, object],
|
||||
kwargs: dict[str, object],
|
||||
) -> T:
|
||||
# To maintain forward compatibility, we need to remove extra fields from
|
||||
# the response.
|
||||
# We will provide another mechanism to allow users to access these fields.
|
||||
|
||||
# For Agent Engine we don't want to call _remove_all_fields because the
|
||||
# user may pass a dict that is not a subclass of BaseModel.
|
||||
# If more modules require we skip this, we may want a different approach
|
||||
should_skip_removing_fields = (
|
||||
kwargs is not None
|
||||
and 'config' in kwargs
|
||||
and kwargs['config'] is not None
|
||||
and isinstance(kwargs['config'], dict)
|
||||
and 'include_all_fields' in kwargs['config']
|
||||
and kwargs['config']['include_all_fields']
|
||||
)
|
||||
|
||||
if not should_skip_removing_fields:
|
||||
_remove_extra_fields(cls, response)
|
||||
validated_response = cls.model_validate(response)
|
||||
return validated_response
|
||||
|
||||
def to_json_dict(self) -> dict[str, object]:
|
||||
return self.model_dump(exclude_none=True, mode='json')
|
||||
|
||||
|
||||
class CaseInSensitiveEnum(str, enum.Enum):
|
||||
"""Case insensitive enum."""
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value: Any) -> Any:
|
||||
try:
|
||||
return cls[value.upper()] # Try to access directly with uppercase
|
||||
except KeyError:
|
||||
try:
|
||||
return cls[value.lower()] # Try to access directly with lowercase
|
||||
except KeyError:
|
||||
warnings.warn(f'{value} is not a valid {cls.__name__}')
|
||||
try:
|
||||
# Creating a enum instance based on the value
|
||||
# We need to use super() to avoid infinite recursion.
|
||||
unknown_enum_val = super().__new__(cls, value)
|
||||
unknown_enum_val._name_ = str(value) # pylint: disable=protected-access
|
||||
unknown_enum_val._value_ = value # pylint: disable=protected-access
|
||||
return unknown_enum_val
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def timestamped_unique_name() -> str:
|
||||
"""Composes a timestamped unique name.
|
||||
|
||||
Returns:
|
||||
A string representing a unique name.
|
||||
"""
|
||||
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
|
||||
unique_id = uuid.uuid4().hex[0:5]
|
||||
return f'{timestamp}_{unique_id}'
|
||||
|
||||
|
||||
def encode_unserializable_types(data: dict[str, object]) -> dict[str, object]:
|
||||
"""Converts unserializable types in dict to json.dumps() compatible types.
|
||||
|
||||
This function is called in models.py after calling convert_to_dict(). The
|
||||
convert_to_dict() can convert pydantic object to dict. However, the input to
|
||||
convert_to_dict() is dict mixed of pydantic object and nested dict(the output
|
||||
of converters). So they may be bytes in the dict and they are out of
|
||||
`ser_json_bytes` control in model_dump(mode='json') called in
|
||||
`convert_to_dict`, as well as datetime deserialization in Pydantic json mode.
|
||||
|
||||
Returns:
|
||||
A dictionary with json.dumps() incompatible type (e.g. bytes datetime)
|
||||
to compatible type (e.g. base64 encoded string, isoformat date string).
|
||||
"""
|
||||
processed_data: dict[str, object] = {}
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
for key, value in data.items():
|
||||
if isinstance(value, bytes):
|
||||
processed_data[key] = base64.urlsafe_b64encode(value).decode('ascii')
|
||||
elif isinstance(value, datetime.datetime):
|
||||
processed_data[key] = value.isoformat()
|
||||
elif isinstance(value, dict):
|
||||
processed_data[key] = encode_unserializable_types(value)
|
||||
elif isinstance(value, list):
|
||||
if all(isinstance(v, bytes) for v in value):
|
||||
processed_data[key] = [
|
||||
base64.urlsafe_b64encode(v).decode('ascii') for v in value
|
||||
]
|
||||
if all(isinstance(v, datetime.datetime) for v in value):
|
||||
processed_data[key] = [v.isoformat() for v in value]
|
||||
else:
|
||||
processed_data[key] = [encode_unserializable_types(v) for v in value]
|
||||
else:
|
||||
processed_data[key] = value
|
||||
return processed_data
|
||||
|
||||
|
||||
def experimental_warning(
|
||||
message: str,
|
||||
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
||||
"""Experimental warning, only warns once."""
|
||||
|
||||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
warning_done = False
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
nonlocal warning_done
|
||||
if not warning_done:
|
||||
warning_done = True
|
||||
warnings.warn(
|
||||
message=message,
|
||||
category=ExperimentalWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _normalize_key_for_matching(key_str: str) -> str:
|
||||
"""Normalizes a key for case-insensitive and snake/camel matching."""
|
||||
return key_str.replace('_', '').lower()
|
||||
|
||||
|
||||
def align_key_case(
|
||||
target_dict: StringDict, update_dict: StringDict
|
||||
) -> StringDict:
|
||||
"""Aligns the keys of update_dict to the case of target_dict keys.
|
||||
|
||||
Args:
|
||||
target_dict: The dictionary with the target key casing.
|
||||
update_dict: The dictionary whose keys need to be aligned.
|
||||
|
||||
Returns:
|
||||
A new dictionary with keys aligned to target_dict's key casing.
|
||||
"""
|
||||
aligned_update_dict: StringDict = {}
|
||||
target_keys_map = {
|
||||
_normalize_key_for_matching(key): key for key in target_dict.keys()
|
||||
}
|
||||
|
||||
for key, value in update_dict.items():
|
||||
normalized_update_key = _normalize_key_for_matching(key)
|
||||
|
||||
if normalized_update_key in target_keys_map:
|
||||
aligned_key = target_keys_map[normalized_update_key]
|
||||
else:
|
||||
aligned_key = key
|
||||
|
||||
if isinstance(value, dict) and isinstance(
|
||||
target_dict.get(aligned_key), dict
|
||||
):
|
||||
aligned_update_dict[aligned_key] = align_key_case(
|
||||
target_dict[aligned_key], value
|
||||
)
|
||||
elif isinstance(value, list) and isinstance(
|
||||
target_dict.get(aligned_key), list
|
||||
):
|
||||
# Direct assign as we treat update_dict list values as golden source.
|
||||
aligned_update_dict[aligned_key] = value
|
||||
else:
|
||||
aligned_update_dict[aligned_key] = value
|
||||
return aligned_update_dict
|
||||
|
||||
|
||||
def recursive_dict_update(
|
||||
target_dict: StringDict, update_dict: StringDict
|
||||
) -> None:
|
||||
"""Recursively updates a target dictionary with values from an update dictionary.
|
||||
|
||||
We don't enforce the updated dict values to have the same type with the
|
||||
target_dict values except log warnings.
|
||||
Users providing the update_dict should be responsible for constructing correct
|
||||
data.
|
||||
|
||||
Args:
|
||||
target_dict (dict): The dictionary to be updated.
|
||||
update_dict (dict): The dictionary containing updates.
|
||||
"""
|
||||
# Python SDK http request may change in camel case or snake case:
|
||||
# If the field is directly set via setv() function, then it is camel case;
|
||||
# otherwise it is snake case.
|
||||
# Align the update_dict key case to target_dict to ensure correct dict update.
|
||||
aligned_update_dict = align_key_case(target_dict, update_dict)
|
||||
for key, value in aligned_update_dict.items():
|
||||
if (
|
||||
key in target_dict
|
||||
and isinstance(target_dict[key], dict)
|
||||
and isinstance(value, dict)
|
||||
):
|
||||
recursive_dict_update(target_dict[key], value)
|
||||
elif key in target_dict and not isinstance(target_dict[key], type(value)):
|
||||
logger.warning(
|
||||
f"Type mismatch for key '{key}'. Existing type:"
|
||||
f' {type(target_dict[key])}, new type: {type(value)}. Overwriting.'
|
||||
)
|
||||
target_dict[key] = value
|
||||
else:
|
||||
target_dict[key] = value
|
||||
|
||||
|
||||
def is_duck_type_of(obj: Any, cls: type[pydantic.BaseModel]) -> bool:
|
||||
"""Checks if an object has all of the fields of a Pydantic model.
|
||||
|
||||
This is a duck-typing alternative to `isinstance` to solve dual-import
|
||||
problems. It returns False for dictionaries, which should be handled by
|
||||
`isinstance(obj, dict)`.
|
||||
|
||||
Args:
|
||||
obj: The object to check.
|
||||
cls: The Pydantic model class to duck-type against.
|
||||
|
||||
Returns:
|
||||
True if the object has all the fields defined in the Pydantic model, False
|
||||
otherwise.
|
||||
"""
|
||||
if isinstance(obj, dict) or not hasattr(cls, 'model_fields'):
|
||||
return False
|
||||
|
||||
# Check if the object has all of the Pydantic model's defined fields.
|
||||
all_matched = all(hasattr(obj, field) for field in cls.model_fields)
|
||||
if not all_matched and isinstance(obj, pydantic.BaseModel):
|
||||
# Check the other way around if obj is a Pydantic model.
|
||||
# Check if the Pydantic model has all of the object's defined fields.
|
||||
try:
|
||||
obj_private = cls()
|
||||
all_matched = all(hasattr(obj_private, f) for f in type(obj).model_fields)
|
||||
except ValueError:
|
||||
return False
|
||||
return all_matched
|
||||
679
venv/lib/python3.12/site-packages/google/genai/_extra_utils.py
Normal file
679
venv/lib/python3.12/site-packages/google/genai/_extra_utils.py
Normal file
@@ -0,0 +1,679 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""Extra utils depending on types that are shared between sync and async modules."""
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import io
|
||||
import logging
|
||||
import sys
|
||||
import typing
|
||||
from typing import Any, Callable, Dict, Optional, Union, get_args, get_origin
|
||||
import mimetypes
|
||||
import os
|
||||
import pydantic
|
||||
|
||||
from . import _common
|
||||
from . import _mcp_utils
|
||||
from . import _transformers as t
|
||||
from . import errors
|
||||
from . import types
|
||||
from ._adapters import McpToGenAiToolAdapter
|
||||
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from types import UnionType
|
||||
else:
|
||||
UnionType = typing._UnionGenericAlias # type: ignore[attr-defined]
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from mcp import ClientSession as McpClientSession
|
||||
from mcp.types import Tool as McpTool
|
||||
else:
|
||||
McpClientSession: typing.Type = Any
|
||||
McpTool: typing.Type = Any
|
||||
try:
|
||||
from mcp import ClientSession as McpClientSession
|
||||
from mcp.types import Tool as McpTool
|
||||
except ImportError:
|
||||
McpClientSession = None
|
||||
McpTool = None
|
||||
|
||||
_DEFAULT_MAX_REMOTE_CALLS_AFC = 10
|
||||
|
||||
logger = logging.getLogger('google_genai.models')
|
||||
|
||||
|
||||
def _create_generate_content_config_model(
|
||||
config: types.GenerateContentConfigOrDict,
|
||||
) -> types.GenerateContentConfig:
|
||||
if isinstance(config, dict):
|
||||
return types.GenerateContentConfig(**config)
|
||||
else:
|
||||
return config
|
||||
|
||||
|
||||
def _get_gcs_uri(
|
||||
src: Union[str, types.BatchJobSourceOrDict]
|
||||
) -> Optional[str]:
|
||||
"""Extracts the first GCS URI from the source, if available."""
|
||||
if isinstance(src, str) and src.startswith('gs://'):
|
||||
return src
|
||||
elif isinstance(src, dict) and src.get('gcs_uri'):
|
||||
return src['gcs_uri'][0] if src['gcs_uri'] else None
|
||||
elif isinstance(src, types.BatchJobSource) and src.gcs_uri:
|
||||
return src.gcs_uri[0] if src.gcs_uri else None
|
||||
return None
|
||||
|
||||
|
||||
def _get_bigquery_uri(
|
||||
src: Union[str, types.BatchJobSourceOrDict]
|
||||
) -> Optional[str]:
|
||||
"""Extracts the BigQuery URI from the source, if available."""
|
||||
if isinstance(src, str) and src.startswith('bq://'):
|
||||
return src
|
||||
elif isinstance(src, dict) and src.get('bigquery_uri'):
|
||||
return src['bigquery_uri']
|
||||
elif isinstance(src, types.BatchJobSource) and src.bigquery_uri:
|
||||
return src.bigquery_uri
|
||||
return None
|
||||
|
||||
|
||||
def format_destination(
|
||||
src: Union[str, types.BatchJobSource],
|
||||
config: Optional[types.CreateBatchJobConfig] = None,
|
||||
) -> types.CreateBatchJobConfig:
|
||||
"""Formats the destination uri based on the source uri for Vertex AI."""
|
||||
if config is None:
|
||||
config = types.CreateBatchJobConfig()
|
||||
|
||||
unique_name = None
|
||||
if not config.display_name:
|
||||
unique_name = _common.timestamped_unique_name()
|
||||
config.display_name = f'genai_batch_job_{unique_name}'
|
||||
|
||||
if not config.dest:
|
||||
gcs_source_uri = _get_gcs_uri(src)
|
||||
bigquery_source_uri = _get_bigquery_uri(src)
|
||||
|
||||
if gcs_source_uri and gcs_source_uri.endswith('.jsonl'):
|
||||
config.dest = f'{gcs_source_uri[:-6]}/dest'
|
||||
elif bigquery_source_uri:
|
||||
unique_name = unique_name or _common.timestamped_unique_name()
|
||||
config.dest = f'{bigquery_source_uri}_dest_{unique_name}'
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def find_afc_incompatible_tool_indexes(
|
||||
config: Optional[types.GenerateContentConfigOrDict] = None,
|
||||
) -> list[int]:
|
||||
"""Checks if the config contains any AFC incompatible tools.
|
||||
|
||||
A `types.Tool` object that contains `function_declarations` is considered a
|
||||
non-AFC tool for this execution path.
|
||||
|
||||
Args:
|
||||
config: The GenerateContentConfig to check for incompatible tools.
|
||||
|
||||
Returns:
|
||||
A list of indexes of the incompatible tools in the config.
|
||||
"""
|
||||
if not config:
|
||||
return []
|
||||
config_model = _create_generate_content_config_model(config)
|
||||
incompatible_tools_indexes: list[int] = []
|
||||
|
||||
if not config_model or not config_model.tools:
|
||||
return incompatible_tools_indexes
|
||||
|
||||
for index, tool in enumerate(config_model.tools):
|
||||
if not isinstance(tool, types.Tool):
|
||||
continue
|
||||
if tool.function_declarations:
|
||||
incompatible_tools_indexes.append(index)
|
||||
if tool.mcp_servers:
|
||||
incompatible_tools_indexes.append(index)
|
||||
return incompatible_tools_indexes
|
||||
|
||||
|
||||
def get_function_map(
|
||||
config: Optional[types.GenerateContentConfigOrDict] = None,
|
||||
mcp_to_genai_tool_adapters: Optional[
|
||||
dict[str, McpToGenAiToolAdapter]
|
||||
] = None,
|
||||
is_caller_method_async: bool = False,
|
||||
) -> dict[str, Union[Callable[..., Any], McpToGenAiToolAdapter]]:
|
||||
"""Returns a function map from the config."""
|
||||
function_map: dict[str, Union[Callable[..., Any], McpToGenAiToolAdapter]] = {}
|
||||
if not config:
|
||||
return function_map
|
||||
config_model = _create_generate_content_config_model(config)
|
||||
if config_model.tools:
|
||||
for tool in config_model.tools:
|
||||
if callable(tool):
|
||||
if inspect.iscoroutinefunction(tool) and not is_caller_method_async:
|
||||
raise errors.UnsupportedFunctionError(
|
||||
f'Function {tool.__name__} is a coroutine function, which is not'
|
||||
' supported for automatic function calling. Please manually'
|
||||
f' invoke {tool.__name__} to get the function response.'
|
||||
)
|
||||
function_map[tool.__name__] = tool
|
||||
if mcp_to_genai_tool_adapters:
|
||||
if not is_caller_method_async:
|
||||
raise errors.UnsupportedFunctionError(
|
||||
'MCP tools are not supported in synchronous methods.'
|
||||
)
|
||||
for tool_name, _ in mcp_to_genai_tool_adapters.items():
|
||||
if function_map.get(tool_name):
|
||||
raise ValueError(
|
||||
f'Tool {tool_name} is already defined for the request.'
|
||||
)
|
||||
function_map.update(mcp_to_genai_tool_adapters)
|
||||
return function_map
|
||||
|
||||
|
||||
def convert_number_values_for_dict_function_call_args(
|
||||
args: _common.StringDict,
|
||||
) -> _common.StringDict:
|
||||
"""Converts float values in dict with no decimal to integers."""
|
||||
return {
|
||||
key: convert_number_values_for_function_call_args(value)
|
||||
for key, value in args.items()
|
||||
}
|
||||
|
||||
|
||||
def convert_number_values_for_function_call_args(
|
||||
args: Union[dict[str, object], list[object], object],
|
||||
) -> Union[dict[str, object], list[object], object]:
|
||||
"""Converts float values with no decimal to integers."""
|
||||
if isinstance(args, float) and args.is_integer():
|
||||
return int(args)
|
||||
if isinstance(args, dict):
|
||||
return {
|
||||
key: convert_number_values_for_function_call_args(value)
|
||||
for key, value in args.items()
|
||||
}
|
||||
if isinstance(args, list):
|
||||
return [
|
||||
convert_number_values_for_function_call_args(value) for value in args
|
||||
]
|
||||
return args
|
||||
|
||||
|
||||
def is_annotation_pydantic_model(annotation: Any) -> bool:
|
||||
try:
|
||||
return inspect.isclass(annotation) and issubclass(
|
||||
annotation, pydantic.BaseModel
|
||||
)
|
||||
# for python 3.10 and below, inspect.isclass(annotation) has inconsistent
|
||||
# results with versions above. for example, inspect.isclass(dict[str, int]) is
|
||||
# True in 3.10 and below but False in 3.11 and above.
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
|
||||
def convert_if_exist_pydantic_model(
|
||||
value: Any, annotation: Any, param_name: str, func_name: str
|
||||
) -> Any:
|
||||
if isinstance(value, dict) and is_annotation_pydantic_model(annotation):
|
||||
try:
|
||||
return annotation(**value)
|
||||
except pydantic.ValidationError as e:
|
||||
raise errors.UnknownFunctionCallArgumentError(
|
||||
f'Failed to parse parameter {param_name} for function'
|
||||
f' {func_name} from function call part because function call argument'
|
||||
f' value {value} is not compatible with parameter annotation'
|
||||
f' {annotation}, due to error {e}'
|
||||
)
|
||||
if isinstance(value, list) and get_origin(annotation) == list:
|
||||
item_type = get_args(annotation)[0]
|
||||
return [
|
||||
convert_if_exist_pydantic_model(item, item_type, param_name, func_name)
|
||||
for item in value
|
||||
]
|
||||
if isinstance(value, dict) and get_origin(annotation) == dict:
|
||||
_, value_type = get_args(annotation)
|
||||
return {
|
||||
k: convert_if_exist_pydantic_model(v, value_type, param_name, func_name)
|
||||
for k, v in value.items()
|
||||
}
|
||||
# example 1: typing.Union[int, float]
|
||||
# example 2: int | float equivalent to UnionType[int, float]
|
||||
if get_origin(annotation) in (Union, UnionType):
|
||||
for arg in get_args(annotation):
|
||||
if (
|
||||
(get_args(arg) and get_origin(arg) is list)
|
||||
or isinstance(value, arg)
|
||||
or (isinstance(value, dict) and is_annotation_pydantic_model(arg))
|
||||
):
|
||||
try:
|
||||
return convert_if_exist_pydantic_model(
|
||||
value, arg, param_name, func_name
|
||||
)
|
||||
# do not raise here because there could be multiple pydantic model types
|
||||
# in the union type.
|
||||
except pydantic.ValidationError:
|
||||
continue
|
||||
# if none of the union type is matched, raise error
|
||||
raise errors.UnknownFunctionCallArgumentError(
|
||||
f'Failed to parse parameter {param_name} for function'
|
||||
f' {func_name} from function call part because function call argument'
|
||||
f' value {value} cannot be converted to parameter annotation'
|
||||
f' {annotation}.'
|
||||
)
|
||||
# the only exception for value and annotation type to be different is int and
|
||||
# float. see convert_number_values_for_function_call_args function for context
|
||||
if isinstance(value, int) and annotation is float:
|
||||
return value
|
||||
if not isinstance(value, annotation):
|
||||
raise errors.UnknownFunctionCallArgumentError(
|
||||
f'Failed to parse parameter {param_name} for function {func_name} from'
|
||||
f' function call part because function call argument value {value} is'
|
||||
f' not compatible with parameter annotation {annotation}.'
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def convert_argument_from_function(
|
||||
args: _common.StringDict, function: Callable[..., Any]
|
||||
) -> _common.StringDict:
|
||||
signature = inspect.signature(function)
|
||||
func_name = function.__name__
|
||||
converted_args = {}
|
||||
for param_name, param in signature.parameters.items():
|
||||
if param_name in args:
|
||||
converted_args[param_name] = convert_if_exist_pydantic_model(
|
||||
args[param_name],
|
||||
param.annotation,
|
||||
param_name,
|
||||
func_name,
|
||||
)
|
||||
return converted_args
|
||||
|
||||
|
||||
def invoke_function_from_dict_args(
|
||||
args: _common.StringDict, function_to_invoke: Callable[..., Any]
|
||||
) -> Any:
|
||||
converted_args = convert_argument_from_function(args, function_to_invoke)
|
||||
try:
|
||||
return function_to_invoke(**converted_args)
|
||||
except Exception as e:
|
||||
raise errors.FunctionInvocationError(
|
||||
f'Failed to invoke function {function_to_invoke.__name__} with'
|
||||
f' converted arguments {converted_args} from model returned function'
|
||||
f' call argument {args} because of error {e}'
|
||||
)
|
||||
|
||||
|
||||
async def invoke_function_from_dict_args_async(
|
||||
args: _common.StringDict, function_to_invoke: Callable[..., Any]
|
||||
) -> Any:
|
||||
converted_args = convert_argument_from_function(args, function_to_invoke)
|
||||
try:
|
||||
return await function_to_invoke(**converted_args)
|
||||
except Exception as e:
|
||||
raise errors.FunctionInvocationError(
|
||||
f'Failed to invoke function {function_to_invoke.__name__} with'
|
||||
f' converted arguments {converted_args} from model returned function'
|
||||
f' call argument {args} because of error {e}'
|
||||
)
|
||||
|
||||
|
||||
def get_function_response_parts(
|
||||
response: types.GenerateContentResponse,
|
||||
function_map: dict[str, Union[Callable[..., Any], McpToGenAiToolAdapter]],
|
||||
) -> list[types.Part]:
|
||||
"""Returns the function response parts from the response."""
|
||||
func_response_parts = []
|
||||
if (
|
||||
response.candidates is not None
|
||||
and isinstance(response.candidates[0].content, types.Content)
|
||||
and response.candidates[0].content.parts is not None
|
||||
):
|
||||
for part in response.candidates[0].content.parts:
|
||||
if not part.function_call:
|
||||
continue
|
||||
func_name = part.function_call.name
|
||||
if func_name is not None and part.function_call.args is not None:
|
||||
func = function_map[func_name]
|
||||
args = convert_number_values_for_dict_function_call_args(
|
||||
part.function_call.args
|
||||
)
|
||||
func_response: _common.StringDict
|
||||
try:
|
||||
if not isinstance(func, McpToGenAiToolAdapter):
|
||||
func_response = {
|
||||
'result': invoke_function_from_dict_args(args, func)
|
||||
}
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
func_response = {'error': str(e)}
|
||||
func_response_part = types.Part.from_function_response(
|
||||
name=func_name, response=func_response
|
||||
)
|
||||
func_response_parts.append(func_response_part)
|
||||
return func_response_parts
|
||||
|
||||
|
||||
async def get_function_response_parts_async(
|
||||
response: types.GenerateContentResponse,
|
||||
function_map: dict[str, Union[Callable[..., Any], McpToGenAiToolAdapter]],
|
||||
) -> list[types.Part]:
|
||||
"""Returns the function response parts from the response."""
|
||||
func_response_parts = []
|
||||
if (
|
||||
response.candidates is not None
|
||||
and isinstance(response.candidates[0].content, types.Content)
|
||||
and response.candidates[0].content.parts is not None
|
||||
):
|
||||
for part in response.candidates[0].content.parts:
|
||||
if not part.function_call:
|
||||
continue
|
||||
func_name = part.function_call.name
|
||||
if func_name is not None and part.function_call.args is not None:
|
||||
func = function_map[func_name]
|
||||
args = convert_number_values_for_dict_function_call_args(
|
||||
part.function_call.args
|
||||
)
|
||||
func_response: _common.StringDict
|
||||
try:
|
||||
if isinstance(func, McpToGenAiToolAdapter):
|
||||
mcp_tool_response = await func.call_tool(
|
||||
types.FunctionCall(name=func_name, args=args)
|
||||
)
|
||||
if mcp_tool_response.isError:
|
||||
func_response = {'error': mcp_tool_response}
|
||||
else:
|
||||
func_response = {'result': mcp_tool_response}
|
||||
elif inspect.iscoroutinefunction(func):
|
||||
func_response = {
|
||||
'result': await invoke_function_from_dict_args_async(args, func)
|
||||
}
|
||||
else:
|
||||
func_response = {
|
||||
'result': await asyncio.to_thread(
|
||||
invoke_function_from_dict_args, args, func
|
||||
)
|
||||
}
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
func_response = {'error': str(e)}
|
||||
func_response_part = types.Part.from_function_response(
|
||||
name=func_name, response=func_response
|
||||
)
|
||||
func_response_parts.append(func_response_part)
|
||||
return func_response_parts
|
||||
|
||||
|
||||
def should_disable_afc(
|
||||
config: Optional[types.GenerateContentConfigOrDict] = None,
|
||||
) -> bool:
|
||||
"""Returns whether automatic function calling is enabled."""
|
||||
if not config:
|
||||
return False
|
||||
config_model = _create_generate_content_config_model(config)
|
||||
# If max_remote_calls is less or equal to 0, warn and disable AFC.
|
||||
if (
|
||||
config_model
|
||||
and config_model.automatic_function_calling
|
||||
and config_model.automatic_function_calling.maximum_remote_calls
|
||||
is not None
|
||||
and int(config_model.automatic_function_calling.maximum_remote_calls) <= 0
|
||||
):
|
||||
logger.warning(
|
||||
'max_remote_calls in automatic_function_calling_config'
|
||||
f' {config_model.automatic_function_calling.maximum_remote_calls} is'
|
||||
' less than or equal to 0. Disabling automatic function calling.'
|
||||
' Please set max_remote_calls to a positive integer.'
|
||||
)
|
||||
return True
|
||||
|
||||
# Default to enable AFC if not specified.
|
||||
if (
|
||||
not config_model.automatic_function_calling
|
||||
or config_model.automatic_function_calling.disable is None
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
config_model.automatic_function_calling.disable
|
||||
and config_model.automatic_function_calling.maximum_remote_calls
|
||||
is not None
|
||||
# exclude the case where max_remote_calls is set to 10 by default.
|
||||
and 'maximum_remote_calls'
|
||||
in config_model.automatic_function_calling.model_fields_set
|
||||
and int(config_model.automatic_function_calling.maximum_remote_calls) > 0
|
||||
):
|
||||
logger.warning(
|
||||
'`automatic_function_calling.disable` is set to `True`. And'
|
||||
' `automatic_function_calling.maximum_remote_calls` is a'
|
||||
' positive number'
|
||||
f' {config_model.automatic_function_calling.maximum_remote_calls}.'
|
||||
' Disabling automatic function calling. If you want to enable'
|
||||
' automatic function calling, please set'
|
||||
' `automatic_function_calling.disable` to `False` or leave it unset,'
|
||||
' and set `automatic_function_calling.maximum_remote_calls` to a'
|
||||
' positive integer or leave'
|
||||
' `automatic_function_calling.maximum_remote_calls` unset.'
|
||||
)
|
||||
|
||||
return config_model.automatic_function_calling.disable
|
||||
|
||||
|
||||
def get_max_remote_calls_afc(
|
||||
config: Optional[types.GenerateContentConfigOrDict] = None,
|
||||
) -> int:
|
||||
if not config:
|
||||
return _DEFAULT_MAX_REMOTE_CALLS_AFC
|
||||
"""Returns the remaining remote calls for automatic function calling."""
|
||||
if should_disable_afc(config):
|
||||
raise ValueError(
|
||||
'automatic function calling is not enabled, but SDK is trying to get'
|
||||
' max remote calls.'
|
||||
)
|
||||
config_model = _create_generate_content_config_model(config)
|
||||
if (
|
||||
not config_model.automatic_function_calling
|
||||
or config_model.automatic_function_calling.maximum_remote_calls is None
|
||||
):
|
||||
return _DEFAULT_MAX_REMOTE_CALLS_AFC
|
||||
return int(config_model.automatic_function_calling.maximum_remote_calls)
|
||||
|
||||
|
||||
def raise_error_for_afc_incompatible_config(config: Optional[types.GenerateContentConfig]
|
||||
) -> None:
|
||||
"""Raises an error if the config is not compatible with AFC."""
|
||||
if (
|
||||
not config
|
||||
or not config.tool_config
|
||||
or not config.tool_config.function_calling_config
|
||||
):
|
||||
return
|
||||
afc_config = config.automatic_function_calling
|
||||
disable_afc_config = afc_config.disable if afc_config else False
|
||||
stream_function_call = (
|
||||
config.tool_config.function_calling_config.stream_function_call_arguments
|
||||
)
|
||||
|
||||
if stream_function_call and not disable_afc_config:
|
||||
raise ValueError(
|
||||
'Running in streaming mode with stream_function_call_arguments'
|
||||
' enabled, this feature is not compatible with automatic function'
|
||||
' calling (AFC). Please set config.automatic_function_calling.disable'
|
||||
' to True to disable AFC or leave config.tool_config.'
|
||||
' function_calling_config.stream_function_call_arguments to be empty'
|
||||
' or set to False to disable streaming function call arguments.'
|
||||
)
|
||||
|
||||
def should_append_afc_history(
|
||||
config: Optional[types.GenerateContentConfigOrDict] = None,
|
||||
) -> bool:
|
||||
if not config:
|
||||
return True
|
||||
config_model = _create_generate_content_config_model(config)
|
||||
if not config_model.automatic_function_calling:
|
||||
return True
|
||||
return not config_model.automatic_function_calling.ignore_call_history
|
||||
|
||||
|
||||
def parse_config_for_mcp_usage(
|
||||
config: Optional[types.GenerateContentConfigOrDict] = None,
|
||||
) -> Optional[types.GenerateContentConfig]:
|
||||
"""Returns a parsed config with an appended MCP header if MCP tools or sessions are used."""
|
||||
if not config:
|
||||
return None
|
||||
config_model = _create_generate_content_config_model(config)
|
||||
# Create a copy of the config model with the tools field cleared since some
|
||||
# tools may not be pickleable.
|
||||
config_model_copy = config_model.model_copy(update={'tools': None})
|
||||
config_model_copy.tools = config_model.tools
|
||||
if config_model.tools and _mcp_utils.has_mcp_tool_usage(config_model.tools):
|
||||
if config_model_copy.http_options is None:
|
||||
config_model_copy.http_options = types.HttpOptions(headers={})
|
||||
if config_model_copy.http_options.headers is None:
|
||||
config_model_copy.http_options.headers = {}
|
||||
_mcp_utils.set_mcp_usage_header(config_model_copy.http_options.headers)
|
||||
|
||||
return config_model_copy
|
||||
|
||||
|
||||
async def parse_config_for_mcp_sessions(
|
||||
config: Optional[types.GenerateContentConfigOrDict] = None,
|
||||
) -> tuple[
|
||||
Optional[types.GenerateContentConfig],
|
||||
dict[str, McpToGenAiToolAdapter],
|
||||
]:
|
||||
"""Returns a parsed config with MCP sessions converted to GenAI tools.
|
||||
|
||||
Also returns a map of MCP tools to GenAI tool adapters to be used for AFC.
|
||||
"""
|
||||
mcp_to_genai_tool_adapters: dict[str, McpToGenAiToolAdapter] = {}
|
||||
parsed_config = parse_config_for_mcp_usage(config)
|
||||
if not parsed_config:
|
||||
return None, mcp_to_genai_tool_adapters
|
||||
# Create a copy of the config model with the tools field cleared as they will
|
||||
# be replaced with the MCP tools converted to GenAI tools.
|
||||
parsed_config_copy = parsed_config.model_copy(update={'tools': None})
|
||||
if parsed_config.tools:
|
||||
parsed_config_copy.tools = []
|
||||
for tool in parsed_config.tools:
|
||||
if McpClientSession is not None and isinstance(tool, McpClientSession):
|
||||
mcp_to_genai_tool_adapter = McpToGenAiToolAdapter(
|
||||
tool, await tool.list_tools()
|
||||
)
|
||||
# Extend the config with the MCP session tools converted to GenAI tools.
|
||||
parsed_config_copy.tools.extend(mcp_to_genai_tool_adapter.tools)
|
||||
for genai_tool in mcp_to_genai_tool_adapter.tools:
|
||||
if genai_tool.function_declarations:
|
||||
for function_declaration in genai_tool.function_declarations:
|
||||
if function_declaration.name:
|
||||
if mcp_to_genai_tool_adapters.get(function_declaration.name):
|
||||
raise ValueError(
|
||||
f'Tool {function_declaration.name} is already defined for'
|
||||
' the request.'
|
||||
)
|
||||
mcp_to_genai_tool_adapters[function_declaration.name] = (
|
||||
mcp_to_genai_tool_adapter
|
||||
)
|
||||
else:
|
||||
parsed_config_copy.tools.append(tool)
|
||||
|
||||
return parsed_config_copy, mcp_to_genai_tool_adapters
|
||||
|
||||
|
||||
def append_chunk_contents(
|
||||
contents: Union[types.ContentListUnion, types.ContentListUnionDict],
|
||||
chunk: types.GenerateContentResponse,
|
||||
) -> Union[types.ContentListUnion, types.ContentListUnionDict]:
|
||||
"""Appends the contents of the chunk to the contents list and returns it."""
|
||||
if chunk is not None and chunk.candidates is not None:
|
||||
chunk_content = chunk.candidates[0].content
|
||||
contents = t.t_contents(contents) # type: ignore[assignment]
|
||||
if isinstance(contents, list) and chunk_content is not None:
|
||||
contents.append(chunk_content) # type: ignore[arg-type]
|
||||
return contents
|
||||
|
||||
|
||||
def prepare_resumable_upload(
|
||||
file: Union[str, os.PathLike[str], io.IOBase],
|
||||
user_http_options: Optional[types.HttpOptionsOrDict] = None,
|
||||
user_mime_type: Optional[str] = None,
|
||||
) -> tuple[
|
||||
types.HttpOptions,
|
||||
int,
|
||||
str,
|
||||
]:
|
||||
"""Prepares the HTTP options, file bytes size and mime type for a resumable upload.
|
||||
|
||||
This function inspects a file (from a path or an in-memory object) to
|
||||
determine its size and MIME type. It then constructs the necessary HTTP
|
||||
headers and options required to initiate a resumable upload session.
|
||||
"""
|
||||
size_bytes = None
|
||||
mime_type = user_mime_type
|
||||
if isinstance(file, io.IOBase):
|
||||
if mime_type is None:
|
||||
raise ValueError(
|
||||
'Unknown mime type: Could not determine the mimetype for your'
|
||||
' file\n please set the `mime_type` argument'
|
||||
)
|
||||
if hasattr(file, 'mode'):
|
||||
if 'b' not in file.mode:
|
||||
raise ValueError('The file must be opened in binary mode.')
|
||||
offset = file.tell()
|
||||
file.seek(0, os.SEEK_END)
|
||||
size_bytes = file.tell() - offset
|
||||
file.seek(offset, os.SEEK_SET)
|
||||
else:
|
||||
fs_path = os.fspath(file)
|
||||
if not fs_path or not os.path.isfile(fs_path):
|
||||
raise FileNotFoundError(f'{file} is not a valid file path.')
|
||||
size_bytes = os.path.getsize(fs_path)
|
||||
if mime_type is None:
|
||||
mime_type, _ = mimetypes.guess_type(fs_path)
|
||||
if mime_type is None:
|
||||
raise ValueError(
|
||||
'Unknown mime type: Could not determine the mimetype for your'
|
||||
' file\n please set the `mime_type` argument'
|
||||
)
|
||||
http_options: types.HttpOptions
|
||||
if user_http_options:
|
||||
if isinstance(user_http_options, dict):
|
||||
user_http_options = types.HttpOptions(**user_http_options)
|
||||
http_options = user_http_options
|
||||
http_options.api_version = ''
|
||||
http_options.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Goog-Upload-Protocol': 'resumable',
|
||||
'X-Goog-Upload-Command': 'start',
|
||||
'X-Goog-Upload-Header-Content-Length': f'{size_bytes}',
|
||||
'X-Goog-Upload-Header-Content-Type': f'{mime_type}',
|
||||
}
|
||||
else:
|
||||
http_options = types.HttpOptions(
|
||||
api_version='',
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Goog-Upload-Protocol': 'resumable',
|
||||
'X-Goog-Upload-Command': 'start',
|
||||
'X-Goog-Upload-Header-Content-Length': f'{size_bytes}',
|
||||
'X-Goog-Upload-Header-Content-Type': f'{mime_type}',
|
||||
},
|
||||
)
|
||||
if isinstance(file, (str, os.PathLike)):
|
||||
if http_options.headers is None:
|
||||
http_options.headers = {}
|
||||
http_options.headers['X-Goog-Upload-File-Name'] = os.path.basename(file)
|
||||
return http_options, size_bytes, mime_type
|
||||
@@ -0,0 +1,120 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
import typing as _t
|
||||
|
||||
from . import types
|
||||
from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given
|
||||
from ._utils import file_from_path
|
||||
from ._client import (
|
||||
Client,
|
||||
Stream,
|
||||
Timeout,
|
||||
Transport,
|
||||
AsyncClient,
|
||||
AsyncStream,
|
||||
RequestOptions,
|
||||
GeminiNextGenAPIClient,
|
||||
AsyncGeminiNextGenAPIClient,
|
||||
)
|
||||
from ._models import BaseModel
|
||||
from ._version import __title__, __version__
|
||||
from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse
|
||||
from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS
|
||||
from ._exceptions import (
|
||||
APIError,
|
||||
ConflictError,
|
||||
NotFoundError,
|
||||
APIStatusError,
|
||||
RateLimitError,
|
||||
APITimeoutError,
|
||||
BadRequestError,
|
||||
APIConnectionError,
|
||||
AuthenticationError,
|
||||
InternalServerError,
|
||||
PermissionDeniedError,
|
||||
UnprocessableEntityError,
|
||||
APIResponseValidationError,
|
||||
GeminiNextGenAPIClientError,
|
||||
)
|
||||
from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient
|
||||
from ._utils._logs import setup_logging as _setup_logging
|
||||
from ._client_adapter import GeminiNextGenAPIClientAdapter, AsyncGeminiNextGenAPIClientAdapter
|
||||
|
||||
__all__ = [
|
||||
"types",
|
||||
"__version__",
|
||||
"__title__",
|
||||
"NoneType",
|
||||
"Transport",
|
||||
"ProxiesTypes",
|
||||
"NotGiven",
|
||||
"NOT_GIVEN",
|
||||
"not_given",
|
||||
"Omit",
|
||||
"omit",
|
||||
"GeminiNextGenAPIClientError",
|
||||
"APIError",
|
||||
"APIStatusError",
|
||||
"APITimeoutError",
|
||||
"APIConnectionError",
|
||||
"APIResponseValidationError",
|
||||
"BadRequestError",
|
||||
"AuthenticationError",
|
||||
"PermissionDeniedError",
|
||||
"NotFoundError",
|
||||
"ConflictError",
|
||||
"UnprocessableEntityError",
|
||||
"RateLimitError",
|
||||
"InternalServerError",
|
||||
"Timeout",
|
||||
"RequestOptions",
|
||||
"Client",
|
||||
"AsyncClient",
|
||||
"Stream",
|
||||
"AsyncStream",
|
||||
"GeminiNextGenAPIClient",
|
||||
"AsyncGeminiNextGenAPIClient",
|
||||
"file_from_path",
|
||||
"BaseModel",
|
||||
"DEFAULT_TIMEOUT",
|
||||
"DEFAULT_MAX_RETRIES",
|
||||
"DEFAULT_CONNECTION_LIMITS",
|
||||
"DefaultHttpxClient",
|
||||
"DefaultAsyncHttpxClient",
|
||||
"DefaultAioHttpClient",
|
||||
"AsyncGeminiNextGenAPIClientAdapter",
|
||||
"GeminiNextGenAPIClientAdapter"
|
||||
]
|
||||
|
||||
if not _t.TYPE_CHECKING:
|
||||
from ._utils._resources_proxy import resources as resources
|
||||
|
||||
_setup_logging()
|
||||
|
||||
# Update the __module__ attribute for exported symbols so that
|
||||
# error messages point to this module instead of the module
|
||||
# it was originally defined in, e.g.
|
||||
# google.genai._interactions._exceptions.NotFoundError -> google.genai._interactions.NotFoundError
|
||||
__locals = locals()
|
||||
for __name in __all__:
|
||||
if not __name.startswith("__"):
|
||||
try:
|
||||
__locals[__name].__module__ = "google.genai._interactions"
|
||||
except (TypeError, AttributeError):
|
||||
# Some of our exported symbols are builtins which we can't set attributes for.
|
||||
pass
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,600 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Mapping
|
||||
from typing_extensions import Self, override
|
||||
|
||||
import httpx
|
||||
|
||||
from . import _exceptions
|
||||
from ._qs import Querystring
|
||||
from ._types import (
|
||||
Omit,
|
||||
Headers,
|
||||
Timeout,
|
||||
NotGiven,
|
||||
Transport,
|
||||
ProxiesTypes,
|
||||
RequestOptions,
|
||||
not_given,
|
||||
)
|
||||
from ._utils import is_given
|
||||
from ._compat import cached_property
|
||||
from ._models import FinalRequestOptions
|
||||
from ._version import __version__
|
||||
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
|
||||
from ._exceptions import APIStatusError
|
||||
from ._base_client import (
|
||||
DEFAULT_MAX_RETRIES,
|
||||
SyncAPIClient,
|
||||
AsyncAPIClient,
|
||||
)
|
||||
from ._client_adapter import GeminiNextGenAPIClientAdapter, AsyncGeminiNextGenAPIClientAdapter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .resources import webhooks, interactions
|
||||
from .resources.webhooks import WebhooksResource, AsyncWebhooksResource
|
||||
from .resources.interactions import InteractionsResource, AsyncInteractionsResource
|
||||
|
||||
__all__ = [
|
||||
"Timeout",
|
||||
"Transport",
|
||||
"ProxiesTypes",
|
||||
"RequestOptions",
|
||||
"GeminiNextGenAPIClient",
|
||||
"AsyncGeminiNextGenAPIClient",
|
||||
"Client",
|
||||
"AsyncClient",
|
||||
]
|
||||
|
||||
|
||||
class GeminiNextGenAPIClient(SyncAPIClient):
|
||||
# client options
|
||||
api_key: str | None
|
||||
api_version: str
|
||||
client_adapter: GeminiNextGenAPIClientAdapter | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str | None = None,
|
||||
api_version: str | None = "v1beta",
|
||||
base_url: str | httpx.URL | None = None,
|
||||
timeout: float | Timeout | None | NotGiven = not_given,
|
||||
max_retries: int = DEFAULT_MAX_RETRIES,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
default_query: Mapping[str, object] | None = None,
|
||||
# Configure a custom httpx client.
|
||||
# We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`.
|
||||
# See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details.
|
||||
http_client: httpx.Client | None = None,
|
||||
client_adapter: GeminiNextGenAPIClientAdapter | None = None,
|
||||
# Enable or disable schema validation for data returned by the API.
|
||||
# When enabled an error APIResponseValidationError is raised
|
||||
# if the API responds with invalid data for the expected schema.
|
||||
#
|
||||
# This parameter may be removed or changed in the future.
|
||||
# If you rely on this feature, please open a GitHub issue
|
||||
# outlining your use-case to help us decide if it should be
|
||||
# part of our public interface in the future.
|
||||
_strict_response_validation: bool = False,
|
||||
) -> None:
|
||||
"""Construct a new synchronous GeminiNextGenAPIClient client instance.
|
||||
|
||||
This automatically infers the `api_key` argument from the `GEMINI_API_KEY` environment variable if it is not provided.
|
||||
"""
|
||||
if api_key is None:
|
||||
api_key = os.environ.get("GEMINI_API_KEY")
|
||||
self.api_key = api_key
|
||||
|
||||
if api_version is None:
|
||||
api_version = "v1beta"
|
||||
self.api_version = api_version
|
||||
|
||||
if base_url is None:
|
||||
base_url = os.environ.get("GEMINI_NEXT_GEN_API_BASE_URL")
|
||||
if base_url is None:
|
||||
base_url = f"https://generativelanguage.googleapis.com"
|
||||
|
||||
self.client_adapter = client_adapter
|
||||
|
||||
super().__init__(
|
||||
version=__version__,
|
||||
base_url=base_url,
|
||||
max_retries=max_retries,
|
||||
timeout=timeout,
|
||||
http_client=http_client,
|
||||
custom_headers=default_headers,
|
||||
custom_query=default_query,
|
||||
_strict_response_validation=_strict_response_validation,
|
||||
)
|
||||
|
||||
self._default_stream_cls = Stream
|
||||
|
||||
@cached_property
|
||||
def interactions(self) -> InteractionsResource:
|
||||
from .resources.interactions import InteractionsResource
|
||||
|
||||
return InteractionsResource(self)
|
||||
|
||||
@cached_property
|
||||
def webhooks(self) -> WebhooksResource:
|
||||
from .resources.webhooks import WebhooksResource
|
||||
|
||||
return WebhooksResource(self)
|
||||
|
||||
@cached_property
|
||||
def with_raw_response(self) -> GeminiNextGenAPIClientWithRawResponse:
|
||||
return GeminiNextGenAPIClientWithRawResponse(self)
|
||||
|
||||
@cached_property
|
||||
def with_streaming_response(self) -> GeminiNextGenAPIClientWithStreamedResponse:
|
||||
return GeminiNextGenAPIClientWithStreamedResponse(self)
|
||||
|
||||
@property
|
||||
@override
|
||||
def qs(self) -> Querystring:
|
||||
return Querystring(array_format="comma")
|
||||
|
||||
@property
|
||||
@override
|
||||
def auth_headers(self) -> dict[str, str]:
|
||||
api_key = self.api_key
|
||||
if api_key is None:
|
||||
return {}
|
||||
return {"x-goog-api-key": api_key}
|
||||
|
||||
@property
|
||||
@override
|
||||
def default_headers(self) -> dict[str, str | Omit]:
|
||||
return {
|
||||
**super().default_headers,
|
||||
**self._custom_headers,
|
||||
}
|
||||
|
||||
@override
|
||||
def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None:
|
||||
if headers.get("Authorization") or custom_headers.get("Authorization") or isinstance(custom_headers.get("Authorization"), Omit):
|
||||
return
|
||||
if self.api_key and headers.get("x-goog-api-key"):
|
||||
return
|
||||
if custom_headers.get("x-goog-api-key") or isinstance(custom_headers.get("x-goog-api-key"), Omit):
|
||||
return
|
||||
|
||||
raise TypeError(
|
||||
'"Could not resolve authentication method. Expected the api_key to be set. Or for the `x-goog-api-key` headers to be explicitly omitted"'
|
||||
)
|
||||
|
||||
@override
|
||||
def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions:
|
||||
if not self.client_adapter or not self.client_adapter.is_vertex_ai():
|
||||
return options
|
||||
|
||||
headers = options.headers or {}
|
||||
has_auth = headers.get("Authorization") or headers.get("x-goog-api-key") # pytype: disable=attribute-error
|
||||
if has_auth:
|
||||
return options
|
||||
|
||||
adapted_headers = self.client_adapter.get_auth_headers()
|
||||
if adapted_headers:
|
||||
options.headers = {
|
||||
**adapted_headers,
|
||||
**headers
|
||||
}
|
||||
return options
|
||||
|
||||
def copy(
|
||||
self,
|
||||
*,
|
||||
api_key: str | None = None,
|
||||
api_version: str | None = None,
|
||||
base_url: str | httpx.URL | None = None,
|
||||
timeout: float | Timeout | None | NotGiven = not_given,
|
||||
http_client: httpx.Client | None = None,
|
||||
max_retries: int | NotGiven = not_given,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
set_default_headers: Mapping[str, str] | None = None,
|
||||
default_query: Mapping[str, object] | None = None,
|
||||
set_default_query: Mapping[str, object] | None = None,
|
||||
client_adapter: GeminiNextGenAPIClientAdapter | None = None,
|
||||
_extra_kwargs: Mapping[str, Any] = {},
|
||||
) -> Self:
|
||||
"""
|
||||
Create a new client instance re-using the same options given to the current client with optional overriding.
|
||||
"""
|
||||
if default_headers is not None and set_default_headers is not None:
|
||||
raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive")
|
||||
|
||||
if default_query is not None and set_default_query is not None:
|
||||
raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive")
|
||||
|
||||
headers = self._custom_headers
|
||||
if default_headers is not None:
|
||||
headers = {**headers, **default_headers}
|
||||
elif set_default_headers is not None:
|
||||
headers = set_default_headers
|
||||
|
||||
params = self._custom_query
|
||||
if default_query is not None:
|
||||
params = {**params, **default_query}
|
||||
elif set_default_query is not None:
|
||||
params = set_default_query
|
||||
|
||||
http_client = http_client or self._client
|
||||
return self.__class__(
|
||||
api_key=api_key or self.api_key,
|
||||
api_version=api_version or self.api_version,
|
||||
base_url=base_url or self.base_url,
|
||||
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
|
||||
http_client=http_client,
|
||||
max_retries=max_retries if is_given(max_retries) else self.max_retries,
|
||||
default_headers=headers,
|
||||
default_query=params,
|
||||
client_adapter=self.client_adapter or client_adapter,
|
||||
**_extra_kwargs,
|
||||
)
|
||||
|
||||
# Alias for `copy` for nicer inline usage, e.g.
|
||||
# client.with_options(timeout=10).foo.create(...)
|
||||
with_options = copy
|
||||
|
||||
def _get_api_version_path_param(self) -> str:
|
||||
return self.api_version
|
||||
|
||||
@override
|
||||
def _make_status_error(
|
||||
self,
|
||||
err_msg: str,
|
||||
*,
|
||||
body: object,
|
||||
response: httpx.Response,
|
||||
) -> APIStatusError:
|
||||
if response.status_code == 400:
|
||||
return _exceptions.BadRequestError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code == 401:
|
||||
return _exceptions.AuthenticationError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code == 403:
|
||||
return _exceptions.PermissionDeniedError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code == 404:
|
||||
return _exceptions.NotFoundError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code == 409:
|
||||
return _exceptions.ConflictError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code == 422:
|
||||
return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code == 429:
|
||||
return _exceptions.RateLimitError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code >= 500:
|
||||
return _exceptions.InternalServerError(err_msg, response=response, body=body)
|
||||
return APIStatusError(err_msg, response=response, body=body)
|
||||
|
||||
|
||||
class AsyncGeminiNextGenAPIClient(AsyncAPIClient):
|
||||
# client options
|
||||
api_key: str | None
|
||||
api_version: str
|
||||
client_adapter: AsyncGeminiNextGenAPIClientAdapter | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str | None = None,
|
||||
api_version: str | None = "v1beta",
|
||||
base_url: str | httpx.URL | None = None,
|
||||
timeout: float | Timeout | None | NotGiven = not_given,
|
||||
max_retries: int = DEFAULT_MAX_RETRIES,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
default_query: Mapping[str, object] | None = None,
|
||||
# Configure a custom httpx client.
|
||||
# We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`.
|
||||
# See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details.
|
||||
http_client: httpx.AsyncClient | None = None,
|
||||
client_adapter: AsyncGeminiNextGenAPIClientAdapter | None = None,
|
||||
# Enable or disable schema validation for data returned by the API.
|
||||
# When enabled an error APIResponseValidationError is raised
|
||||
# if the API responds with invalid data for the expected schema.
|
||||
#
|
||||
# This parameter may be removed or changed in the future.
|
||||
# If you rely on this feature, please open a GitHub issue
|
||||
# outlining your use-case to help us decide if it should be
|
||||
# part of our public interface in the future.
|
||||
_strict_response_validation: bool = False,
|
||||
) -> None:
|
||||
"""Construct a new async AsyncGeminiNextGenAPIClient client instance.
|
||||
|
||||
This automatically infers the `api_key` argument from the `GEMINI_API_KEY` environment variable if it is not provided.
|
||||
"""
|
||||
if api_key is None:
|
||||
api_key = os.environ.get("GEMINI_API_KEY")
|
||||
self.api_key = api_key
|
||||
|
||||
if api_version is None:
|
||||
api_version = "v1beta"
|
||||
self.api_version = api_version
|
||||
|
||||
if base_url is None:
|
||||
base_url = os.environ.get("GEMINI_NEXT_GEN_API_BASE_URL")
|
||||
if base_url is None:
|
||||
base_url = f"https://generativelanguage.googleapis.com"
|
||||
|
||||
self.client_adapter = client_adapter
|
||||
|
||||
super().__init__(
|
||||
version=__version__,
|
||||
base_url=base_url,
|
||||
max_retries=max_retries,
|
||||
timeout=timeout,
|
||||
http_client=http_client,
|
||||
custom_headers=default_headers,
|
||||
custom_query=default_query,
|
||||
_strict_response_validation=_strict_response_validation,
|
||||
)
|
||||
|
||||
self._default_stream_cls = AsyncStream
|
||||
|
||||
@cached_property
|
||||
def interactions(self) -> AsyncInteractionsResource:
|
||||
from .resources.interactions import AsyncInteractionsResource
|
||||
|
||||
return AsyncInteractionsResource(self)
|
||||
|
||||
@cached_property
|
||||
def webhooks(self) -> AsyncWebhooksResource:
|
||||
from .resources.webhooks import AsyncWebhooksResource
|
||||
|
||||
return AsyncWebhooksResource(self)
|
||||
|
||||
@cached_property
|
||||
def with_raw_response(self) -> AsyncGeminiNextGenAPIClientWithRawResponse:
|
||||
return AsyncGeminiNextGenAPIClientWithRawResponse(self)
|
||||
|
||||
@cached_property
|
||||
def with_streaming_response(self) -> AsyncGeminiNextGenAPIClientWithStreamedResponse:
|
||||
return AsyncGeminiNextGenAPIClientWithStreamedResponse(self)
|
||||
|
||||
@property
|
||||
@override
|
||||
def qs(self) -> Querystring:
|
||||
return Querystring(array_format="comma")
|
||||
|
||||
@property
|
||||
@override
|
||||
def auth_headers(self) -> dict[str, str]:
|
||||
api_key = self.api_key
|
||||
if api_key is None:
|
||||
return {}
|
||||
return {"x-goog-api-key": api_key}
|
||||
|
||||
@property
|
||||
@override
|
||||
def default_headers(self) -> dict[str, str | Omit]:
|
||||
return {
|
||||
**super().default_headers,
|
||||
**self._custom_headers,
|
||||
}
|
||||
|
||||
@override
|
||||
def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None:
|
||||
if headers.get("Authorization") or custom_headers.get("Authorization") or isinstance(custom_headers.get("Authorization"), Omit):
|
||||
return
|
||||
if self.api_key and headers.get("x-goog-api-key"):
|
||||
return
|
||||
if custom_headers.get("x-goog-api-key") or isinstance(custom_headers.get("x-goog-api-key"), Omit):
|
||||
return
|
||||
|
||||
raise TypeError(
|
||||
'"Could not resolve authentication method. Expected the api_key to be set. Or for the `x-goog-api-key` headers to be explicitly omitted"'
|
||||
)
|
||||
|
||||
@override
|
||||
async def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions:
|
||||
if not self.client_adapter or not self.client_adapter.is_vertex_ai():
|
||||
return options
|
||||
|
||||
headers = options.headers or {}
|
||||
has_auth = headers.get("Authorization") or headers.get("x-goog-api-key") # pytype: disable=attribute-error
|
||||
if has_auth:
|
||||
return options
|
||||
|
||||
adapted_headers = await self.client_adapter.async_get_auth_headers()
|
||||
if adapted_headers:
|
||||
options.headers = {
|
||||
**adapted_headers,
|
||||
**headers
|
||||
}
|
||||
return options
|
||||
|
||||
def copy(
|
||||
self,
|
||||
*,
|
||||
api_key: str | None = None,
|
||||
api_version: str | None = None,
|
||||
base_url: str | httpx.URL | None = None,
|
||||
timeout: float | Timeout | None | NotGiven = not_given,
|
||||
http_client: httpx.AsyncClient | None = None,
|
||||
max_retries: int | NotGiven = not_given,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
set_default_headers: Mapping[str, str] | None = None,
|
||||
default_query: Mapping[str, object] | None = None,
|
||||
set_default_query: Mapping[str, object] | None = None,
|
||||
client_adapter: AsyncGeminiNextGenAPIClientAdapter | None = None,
|
||||
_extra_kwargs: Mapping[str, Any] = {},
|
||||
) -> Self:
|
||||
"""
|
||||
Create a new client instance re-using the same options given to the current client with optional overriding.
|
||||
"""
|
||||
if default_headers is not None and set_default_headers is not None:
|
||||
raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive")
|
||||
|
||||
if default_query is not None and set_default_query is not None:
|
||||
raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive")
|
||||
|
||||
headers = self._custom_headers
|
||||
if default_headers is not None:
|
||||
headers = {**headers, **default_headers}
|
||||
elif set_default_headers is not None:
|
||||
headers = set_default_headers
|
||||
|
||||
params = self._custom_query
|
||||
if default_query is not None:
|
||||
params = {**params, **default_query}
|
||||
elif set_default_query is not None:
|
||||
params = set_default_query
|
||||
|
||||
http_client = http_client or self._client
|
||||
return self.__class__(
|
||||
api_key=api_key or self.api_key,
|
||||
api_version=api_version or self.api_version,
|
||||
base_url=base_url or self.base_url,
|
||||
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
|
||||
http_client=http_client,
|
||||
max_retries=max_retries if is_given(max_retries) else self.max_retries,
|
||||
default_headers=headers,
|
||||
default_query=params,
|
||||
client_adapter=self.client_adapter or client_adapter,
|
||||
**_extra_kwargs,
|
||||
)
|
||||
|
||||
# Alias for `copy` for nicer inline usage, e.g.
|
||||
# client.with_options(timeout=10).foo.create(...)
|
||||
with_options = copy
|
||||
|
||||
def _get_api_version_path_param(self) -> str:
|
||||
return self.api_version
|
||||
|
||||
@override
|
||||
def _make_status_error(
|
||||
self,
|
||||
err_msg: str,
|
||||
*,
|
||||
body: object,
|
||||
response: httpx.Response,
|
||||
) -> APIStatusError:
|
||||
if response.status_code == 400:
|
||||
return _exceptions.BadRequestError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code == 401:
|
||||
return _exceptions.AuthenticationError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code == 403:
|
||||
return _exceptions.PermissionDeniedError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code == 404:
|
||||
return _exceptions.NotFoundError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code == 409:
|
||||
return _exceptions.ConflictError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code == 422:
|
||||
return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code == 429:
|
||||
return _exceptions.RateLimitError(err_msg, response=response, body=body)
|
||||
|
||||
if response.status_code >= 500:
|
||||
return _exceptions.InternalServerError(err_msg, response=response, body=body)
|
||||
return APIStatusError(err_msg, response=response, body=body)
|
||||
|
||||
|
||||
class GeminiNextGenAPIClientWithRawResponse:
|
||||
_client: GeminiNextGenAPIClient
|
||||
|
||||
def __init__(self, client: GeminiNextGenAPIClient) -> None:
|
||||
self._client = client
|
||||
|
||||
@cached_property
|
||||
def interactions(self) -> interactions.InteractionsResourceWithRawResponse:
|
||||
from .resources.interactions import InteractionsResourceWithRawResponse
|
||||
|
||||
return InteractionsResourceWithRawResponse(self._client.interactions)
|
||||
|
||||
@cached_property
|
||||
def webhooks(self) -> webhooks.WebhooksResourceWithRawResponse:
|
||||
from .resources.webhooks import WebhooksResourceWithRawResponse
|
||||
|
||||
return WebhooksResourceWithRawResponse(self._client.webhooks)
|
||||
|
||||
|
||||
class AsyncGeminiNextGenAPIClientWithRawResponse:
|
||||
_client: AsyncGeminiNextGenAPIClient
|
||||
|
||||
def __init__(self, client: AsyncGeminiNextGenAPIClient) -> None:
|
||||
self._client = client
|
||||
|
||||
@cached_property
|
||||
def interactions(self) -> interactions.AsyncInteractionsResourceWithRawResponse:
|
||||
from .resources.interactions import AsyncInteractionsResourceWithRawResponse
|
||||
|
||||
return AsyncInteractionsResourceWithRawResponse(self._client.interactions)
|
||||
|
||||
@cached_property
|
||||
def webhooks(self) -> webhooks.AsyncWebhooksResourceWithRawResponse:
|
||||
from .resources.webhooks import AsyncWebhooksResourceWithRawResponse
|
||||
|
||||
return AsyncWebhooksResourceWithRawResponse(self._client.webhooks)
|
||||
|
||||
|
||||
class GeminiNextGenAPIClientWithStreamedResponse:
|
||||
_client: GeminiNextGenAPIClient
|
||||
|
||||
def __init__(self, client: GeminiNextGenAPIClient) -> None:
|
||||
self._client = client
|
||||
|
||||
@cached_property
|
||||
def interactions(self) -> interactions.InteractionsResourceWithStreamingResponse:
|
||||
from .resources.interactions import InteractionsResourceWithStreamingResponse
|
||||
|
||||
return InteractionsResourceWithStreamingResponse(self._client.interactions)
|
||||
|
||||
@cached_property
|
||||
def webhooks(self) -> webhooks.WebhooksResourceWithStreamingResponse:
|
||||
from .resources.webhooks import WebhooksResourceWithStreamingResponse
|
||||
|
||||
return WebhooksResourceWithStreamingResponse(self._client.webhooks)
|
||||
|
||||
|
||||
class AsyncGeminiNextGenAPIClientWithStreamedResponse:
|
||||
_client: AsyncGeminiNextGenAPIClient
|
||||
|
||||
def __init__(self, client: AsyncGeminiNextGenAPIClient) -> None:
|
||||
self._client = client
|
||||
|
||||
@cached_property
|
||||
def interactions(self) -> interactions.AsyncInteractionsResourceWithStreamingResponse:
|
||||
from .resources.interactions import AsyncInteractionsResourceWithStreamingResponse
|
||||
|
||||
return AsyncInteractionsResourceWithStreamingResponse(self._client.interactions)
|
||||
|
||||
@cached_property
|
||||
def webhooks(self) -> webhooks.AsyncWebhooksResourceWithStreamingResponse:
|
||||
from .resources.webhooks import AsyncWebhooksResourceWithStreamingResponse
|
||||
|
||||
return AsyncWebhooksResourceWithStreamingResponse(self._client.webhooks)
|
||||
|
||||
|
||||
Client = GeminiNextGenAPIClient
|
||||
|
||||
AsyncClient = AsyncGeminiNextGenAPIClient
|
||||
@@ -0,0 +1,48 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
__all__ = [
|
||||
"GeminiNextGenAPIClientAdapter",
|
||||
"AsyncGeminiNextGenAPIClientAdapter"
|
||||
]
|
||||
|
||||
class BaseGeminiNextGenAPIClientAdapter(ABC):
|
||||
@abstractmethod
|
||||
def is_vertex_ai(self) -> bool:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_project(self) -> str | None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_location(self) -> str | None:
|
||||
...
|
||||
|
||||
|
||||
class AsyncGeminiNextGenAPIClientAdapter(BaseGeminiNextGenAPIClientAdapter):
|
||||
@abstractmethod
|
||||
async def async_get_auth_headers(self) -> dict[str, str] | None:
|
||||
...
|
||||
|
||||
|
||||
class GeminiNextGenAPIClientAdapter(BaseGeminiNextGenAPIClientAdapter):
|
||||
@abstractmethod
|
||||
def get_auth_headers(self) -> dict[str, str] | None:
|
||||
...
|
||||
@@ -0,0 +1,241 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload
|
||||
from datetime import date, datetime
|
||||
from typing_extensions import Self, Literal, TypedDict
|
||||
|
||||
import pydantic
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
from ._types import IncEx, StrBytesIntFloat
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel)
|
||||
|
||||
# --------------- Pydantic v2, v3 compatibility ---------------
|
||||
|
||||
# Pyright incorrectly reports some of our functions as overriding a method when they don't
|
||||
# pyright: reportIncompatibleMethodOverride=false
|
||||
|
||||
PYDANTIC_V1 = pydantic.VERSION.startswith("1.")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001
|
||||
...
|
||||
|
||||
def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: # noqa: ARG001
|
||||
...
|
||||
|
||||
def get_args(t: type[Any]) -> tuple[Any, ...]: # noqa: ARG001
|
||||
...
|
||||
|
||||
def is_union(tp: type[Any] | None) -> bool: # noqa: ARG001
|
||||
...
|
||||
|
||||
def get_origin(t: type[Any]) -> type[Any] | None: # noqa: ARG001
|
||||
...
|
||||
|
||||
def is_literal_type(type_: type[Any]) -> bool: # noqa: ARG001
|
||||
...
|
||||
|
||||
def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001
|
||||
...
|
||||
|
||||
else:
|
||||
# v1 re-exports
|
||||
if PYDANTIC_V1:
|
||||
from pydantic.typing import (
|
||||
get_args as get_args,
|
||||
is_union as is_union,
|
||||
get_origin as get_origin,
|
||||
is_typeddict as is_typeddict,
|
||||
is_literal_type as is_literal_type,
|
||||
)
|
||||
from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime
|
||||
else:
|
||||
from ._utils import (
|
||||
get_args as get_args,
|
||||
is_union as is_union,
|
||||
get_origin as get_origin,
|
||||
parse_date as parse_date,
|
||||
is_typeddict as is_typeddict,
|
||||
parse_datetime as parse_datetime,
|
||||
is_literal_type as is_literal_type,
|
||||
)
|
||||
|
||||
|
||||
# refactored config
|
||||
if TYPE_CHECKING:
|
||||
from pydantic import ConfigDict as ConfigDict
|
||||
else:
|
||||
if PYDANTIC_V1:
|
||||
# TODO: provide an error message here?
|
||||
ConfigDict = None
|
||||
else:
|
||||
from pydantic import ConfigDict as ConfigDict
|
||||
|
||||
|
||||
# renamed methods / properties
|
||||
def parse_obj(model: type[_ModelT], value: object) -> _ModelT:
|
||||
if PYDANTIC_V1:
|
||||
return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
|
||||
else:
|
||||
return model.model_validate(value)
|
||||
|
||||
|
||||
def field_is_required(field: FieldInfo) -> bool:
|
||||
if PYDANTIC_V1:
|
||||
return field.required # type: ignore
|
||||
return field.is_required()
|
||||
|
||||
|
||||
def field_get_default(field: FieldInfo) -> Any:
|
||||
value = field.get_default()
|
||||
if PYDANTIC_V1:
|
||||
return value
|
||||
from pydantic_core import PydanticUndefined
|
||||
|
||||
if value == PydanticUndefined:
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def field_outer_type(field: FieldInfo) -> Any:
|
||||
if PYDANTIC_V1:
|
||||
return field.outer_type_ # type: ignore
|
||||
return field.annotation
|
||||
|
||||
|
||||
def get_model_config(model: type[pydantic.BaseModel]) -> Any:
|
||||
if PYDANTIC_V1:
|
||||
return model.__config__ # type: ignore
|
||||
return model.model_config
|
||||
|
||||
|
||||
def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]:
|
||||
if PYDANTIC_V1:
|
||||
return model.__fields__ # type: ignore
|
||||
return model.model_fields
|
||||
|
||||
|
||||
def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT:
|
||||
if PYDANTIC_V1:
|
||||
return model.copy(deep=deep) # type: ignore
|
||||
return model.model_copy(deep=deep)
|
||||
|
||||
|
||||
def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
|
||||
if PYDANTIC_V1:
|
||||
return model.json(indent=indent) # type: ignore
|
||||
return model.model_dump_json(indent=indent)
|
||||
|
||||
|
||||
class _ModelDumpKwargs(TypedDict, total=False):
|
||||
by_alias: bool
|
||||
|
||||
|
||||
def model_dump(
|
||||
model: pydantic.BaseModel,
|
||||
*,
|
||||
exclude: IncEx | None = None,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
warnings: bool = True,
|
||||
mode: Literal["json", "python"] = "python",
|
||||
by_alias: bool | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
|
||||
kwargs: _ModelDumpKwargs = {}
|
||||
if by_alias is not None:
|
||||
kwargs["by_alias"] = by_alias
|
||||
return model.model_dump(
|
||||
mode=mode,
|
||||
exclude=exclude,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
# warnings are not supported in Pydantic v1
|
||||
warnings=True if PYDANTIC_V1 else warnings,
|
||||
**kwargs,
|
||||
)
|
||||
return cast(
|
||||
"dict[str, Any]",
|
||||
model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
|
||||
exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def model_parse(model: type[_ModelT], data: Any) -> _ModelT:
|
||||
if PYDANTIC_V1:
|
||||
return model.parse_obj(data) # pyright: ignore[reportDeprecated]
|
||||
return model.model_validate(data)
|
||||
|
||||
|
||||
# generic models
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class GenericModel(pydantic.BaseModel): ...
|
||||
|
||||
else:
|
||||
if PYDANTIC_V1:
|
||||
import pydantic.generics
|
||||
|
||||
class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ...
|
||||
else:
|
||||
# there no longer needs to be a distinction in v2 but
|
||||
# we still have to create our own subclass to avoid
|
||||
# inconsistent MRO ordering errors
|
||||
class GenericModel(pydantic.BaseModel): ...
|
||||
|
||||
|
||||
# cached properties
|
||||
if TYPE_CHECKING:
|
||||
cached_property = property
|
||||
|
||||
# we define a separate type (copied from typeshed)
|
||||
# that represents that `cached_property` is `set`able
|
||||
# at runtime, which differs from `@property`.
|
||||
#
|
||||
# this is a separate type as editors likely special case
|
||||
# `@property` and we don't want to cause issues just to have
|
||||
# more helpful internal types.
|
||||
|
||||
class typed_cached_property(Generic[_T]):
|
||||
func: Callable[[Any], _T]
|
||||
attrname: str | None
|
||||
|
||||
def __init__(self, func: Callable[[Any], _T]) -> None: ...
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ...
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ...
|
||||
|
||||
def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self:
|
||||
raise NotImplementedError()
|
||||
|
||||
def __set_name__(self, owner: type[Any], name: str) -> None: ...
|
||||
|
||||
# __set__ is not defined at runtime, but @cached_property is designed to be settable
|
||||
def __set__(self, instance: object, value: _T) -> None: ...
|
||||
else:
|
||||
from functools import cached_property as cached_property
|
||||
|
||||
typed_cached_property = cached_property
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
import httpx
|
||||
|
||||
RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response"
|
||||
OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to"
|
||||
|
||||
# default timeout is 1 minute
|
||||
DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0)
|
||||
DEFAULT_MAX_RETRIES = 2
|
||||
DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20)
|
||||
|
||||
INITIAL_RETRY_DELAY = 0.5
|
||||
MAX_RETRY_DELAY = 8.0
|
||||
@@ -0,0 +1,123 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing_extensions import Literal
|
||||
|
||||
import httpx
|
||||
|
||||
__all__ = [
|
||||
"BadRequestError",
|
||||
"AuthenticationError",
|
||||
"PermissionDeniedError",
|
||||
"NotFoundError",
|
||||
"ConflictError",
|
||||
"UnprocessableEntityError",
|
||||
"RateLimitError",
|
||||
"InternalServerError",
|
||||
]
|
||||
|
||||
|
||||
class GeminiNextGenAPIClientError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class APIError(GeminiNextGenAPIClientError):
|
||||
message: str
|
||||
request: httpx.Request
|
||||
|
||||
body: object | None
|
||||
"""The API response body.
|
||||
|
||||
If the API responded with a valid JSON structure then this property will be the
|
||||
decoded result.
|
||||
|
||||
If it isn't a valid JSON structure then this will be the raw response.
|
||||
|
||||
If there was no response associated with this error then it will be `None`.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None: # noqa: ARG002
|
||||
super().__init__(message)
|
||||
self.request = request
|
||||
self.message = message
|
||||
self.body = body
|
||||
|
||||
|
||||
class APIResponseValidationError(APIError):
|
||||
response: httpx.Response
|
||||
status_code: int
|
||||
|
||||
def __init__(self, response: httpx.Response, body: object | None, *, message: str | None = None) -> None:
|
||||
super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body)
|
||||
self.response = response
|
||||
self.status_code = response.status_code
|
||||
|
||||
|
||||
class APIStatusError(APIError):
|
||||
"""Raised when an API response has a status code of 4xx or 5xx."""
|
||||
|
||||
response: httpx.Response
|
||||
status_code: int
|
||||
|
||||
def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None:
|
||||
super().__init__(message, response.request, body=body)
|
||||
self.response = response
|
||||
self.status_code = response.status_code
|
||||
|
||||
|
||||
class APIConnectionError(APIError):
|
||||
def __init__(self, *, message: str = "Connection error.", request: httpx.Request) -> None:
|
||||
super().__init__(message, request, body=None)
|
||||
|
||||
|
||||
class APITimeoutError(APIConnectionError):
|
||||
def __init__(self, request: httpx.Request) -> None:
|
||||
super().__init__(message="Request timed out.", request=request)
|
||||
|
||||
|
||||
class BadRequestError(APIStatusError):
|
||||
status_code: Literal[400] = 400 # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
|
||||
class AuthenticationError(APIStatusError):
|
||||
status_code: Literal[401] = 401 # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
|
||||
class PermissionDeniedError(APIStatusError):
|
||||
status_code: Literal[403] = 403 # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
|
||||
class NotFoundError(APIStatusError):
|
||||
status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
|
||||
class ConflictError(APIStatusError):
|
||||
status_code: Literal[409] = 409 # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
|
||||
class UnprocessableEntityError(APIStatusError):
|
||||
status_code: Literal[422] = 422 # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
|
||||
class RateLimitError(APIStatusError):
|
||||
status_code: Literal[429] = 429 # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
|
||||
class InternalServerError(APIStatusError):
|
||||
pass
|
||||
@@ -0,0 +1,139 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
from typing import overload
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
import anyio
|
||||
|
||||
from ._types import (
|
||||
FileTypes,
|
||||
FileContent,
|
||||
RequestFiles,
|
||||
HttpxFileTypes,
|
||||
Base64FileInput,
|
||||
HttpxFileContent,
|
||||
HttpxRequestFiles,
|
||||
)
|
||||
from ._utils import is_tuple_t, is_mapping_t, is_sequence_t
|
||||
|
||||
|
||||
def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]:
|
||||
return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike)
|
||||
|
||||
|
||||
def is_file_content(obj: object) -> TypeGuard[FileContent]:
|
||||
return (
|
||||
isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike)
|
||||
)
|
||||
|
||||
|
||||
def assert_is_file_content(obj: object, *, key: str | None = None) -> None:
|
||||
if not is_file_content(obj):
|
||||
prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`"
|
||||
raise RuntimeError(
|
||||
f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead."
|
||||
) from None
|
||||
|
||||
|
||||
@overload
|
||||
def to_httpx_files(files: None) -> None: ...
|
||||
|
||||
|
||||
@overload
|
||||
def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ...
|
||||
|
||||
|
||||
def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None:
|
||||
if files is None:
|
||||
return None
|
||||
|
||||
if is_mapping_t(files):
|
||||
files = {key: _transform_file(file) for key, file in files.items()}
|
||||
elif is_sequence_t(files):
|
||||
files = [(key, _transform_file(file)) for key, file in files]
|
||||
else:
|
||||
raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence")
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def _transform_file(file: FileTypes) -> HttpxFileTypes:
|
||||
if is_file_content(file):
|
||||
if isinstance(file, os.PathLike):
|
||||
path = pathlib.Path(file)
|
||||
return (path.name, path.read_bytes())
|
||||
|
||||
return file
|
||||
|
||||
if is_tuple_t(file):
|
||||
return (file[0], read_file_content(file[1]), *file[2:])
|
||||
|
||||
raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
|
||||
|
||||
|
||||
def read_file_content(file: FileContent) -> HttpxFileContent:
|
||||
if isinstance(file, os.PathLike):
|
||||
return pathlib.Path(file).read_bytes()
|
||||
return file
|
||||
|
||||
|
||||
@overload
|
||||
async def async_to_httpx_files(files: None) -> None: ...
|
||||
|
||||
|
||||
@overload
|
||||
async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ...
|
||||
|
||||
|
||||
async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None:
|
||||
if files is None:
|
||||
return None
|
||||
|
||||
if is_mapping_t(files):
|
||||
files = {key: await _async_transform_file(file) for key, file in files.items()}
|
||||
elif is_sequence_t(files):
|
||||
files = [(key, await _async_transform_file(file)) for key, file in files]
|
||||
else:
|
||||
raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence")
|
||||
|
||||
return files
|
||||
|
||||
|
||||
async def _async_transform_file(file: FileTypes) -> HttpxFileTypes:
|
||||
if is_file_content(file):
|
||||
if isinstance(file, os.PathLike):
|
||||
path = anyio.Path(file)
|
||||
return (path.name, await path.read_bytes())
|
||||
|
||||
return file
|
||||
|
||||
if is_tuple_t(file):
|
||||
return (file[0], await async_read_file_content(file[1]), *file[2:])
|
||||
|
||||
raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
|
||||
|
||||
|
||||
async def async_read_file_content(file: FileContent) -> HttpxFileContent:
|
||||
if isinstance(file, os.PathLike):
|
||||
return await anyio.Path(file).read_bytes()
|
||||
|
||||
return file
|
||||
@@ -0,0 +1,888 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import inspect
|
||||
import weakref
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Type,
|
||||
Union,
|
||||
Generic,
|
||||
TypeVar,
|
||||
Callable,
|
||||
Iterable,
|
||||
Optional,
|
||||
AsyncIterable,
|
||||
cast,
|
||||
)
|
||||
from datetime import date, datetime
|
||||
from typing_extensions import (
|
||||
List,
|
||||
Unpack,
|
||||
Literal,
|
||||
ClassVar,
|
||||
Protocol,
|
||||
Required,
|
||||
ParamSpec,
|
||||
TypedDict,
|
||||
TypeGuard,
|
||||
final,
|
||||
override,
|
||||
runtime_checkable,
|
||||
)
|
||||
|
||||
import pydantic
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
from ._types import (
|
||||
Body,
|
||||
IncEx,
|
||||
Query,
|
||||
ModelT,
|
||||
Headers,
|
||||
Timeout,
|
||||
NotGiven,
|
||||
AnyMapping,
|
||||
HttpxRequestFiles,
|
||||
)
|
||||
from ._utils import (
|
||||
PropertyInfo,
|
||||
is_list,
|
||||
is_given,
|
||||
json_safe,
|
||||
lru_cache,
|
||||
is_mapping,
|
||||
parse_date,
|
||||
coerce_boolean,
|
||||
parse_datetime,
|
||||
strip_not_given,
|
||||
extract_type_arg,
|
||||
is_annotated_type,
|
||||
is_type_alias_type,
|
||||
strip_annotated_type,
|
||||
)
|
||||
from ._compat import (
|
||||
PYDANTIC_V1,
|
||||
ConfigDict,
|
||||
GenericModel as BaseGenericModel,
|
||||
get_args,
|
||||
is_union,
|
||||
parse_obj,
|
||||
get_origin,
|
||||
is_literal_type,
|
||||
get_model_config,
|
||||
get_model_fields,
|
||||
field_get_default,
|
||||
)
|
||||
from ._constants import RAW_RESPONSE_HEADER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema
|
||||
|
||||
__all__ = ["BaseModel", "GenericModel"]
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel")
|
||||
|
||||
P = ParamSpec("P")
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class _ConfigProtocol(Protocol):
|
||||
allow_population_by_field_name: bool
|
||||
|
||||
|
||||
class BaseModel(pydantic.BaseModel):
|
||||
if PYDANTIC_V1:
|
||||
|
||||
@property
|
||||
@override
|
||||
def model_fields_set(self) -> set[str]:
|
||||
# a forwards-compat shim for pydantic v2
|
||||
return self.__fields_set__ # type: ignore
|
||||
|
||||
class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
|
||||
extra: Any = pydantic.Extra.allow # type: ignore
|
||||
else:
|
||||
model_config: ClassVar[ConfigDict] = ConfigDict(
|
||||
extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true"))
|
||||
)
|
||||
|
||||
def to_dict(
|
||||
self,
|
||||
*,
|
||||
mode: Literal["json", "python"] = "python",
|
||||
use_api_names: bool = True,
|
||||
exclude_unset: bool = True,
|
||||
exclude_defaults: bool = False,
|
||||
exclude_none: bool = False,
|
||||
warnings: bool = True,
|
||||
) -> dict[str, object]:
|
||||
"""Recursively generate a dictionary representation of the model, optionally specifying which fields to include or exclude.
|
||||
|
||||
By default, fields that were not set by the API will not be included,
|
||||
and keys will match the API response, *not* the property names from the model.
|
||||
|
||||
For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property,
|
||||
the output will use the `"fooBar"` key (unless `use_api_names=False` is passed).
|
||||
|
||||
Args:
|
||||
mode:
|
||||
If mode is 'json', the dictionary will only contain JSON serializable types. e.g. `datetime` will be turned into a string, `"2024-3-22T18:11:19.117000Z"`.
|
||||
If mode is 'python', the dictionary may contain any Python objects. e.g. `datetime(2024, 3, 22)`
|
||||
|
||||
use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`.
|
||||
exclude_unset: Whether to exclude fields that have not been explicitly set.
|
||||
exclude_defaults: Whether to exclude fields that are set to their default value from the output.
|
||||
exclude_none: Whether to exclude fields that have a value of `None` from the output.
|
||||
warnings: Whether to log warnings when invalid fields are encountered. This is only supported in Pydantic v2.
|
||||
"""
|
||||
return self.model_dump(
|
||||
mode=mode,
|
||||
by_alias=use_api_names,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
def to_json(
|
||||
self,
|
||||
*,
|
||||
indent: int | None = 2,
|
||||
use_api_names: bool = True,
|
||||
exclude_unset: bool = True,
|
||||
exclude_defaults: bool = False,
|
||||
exclude_none: bool = False,
|
||||
warnings: bool = True,
|
||||
) -> str:
|
||||
"""Generates a JSON string representing this model as it would be received from or sent to the API (but with indentation).
|
||||
|
||||
By default, fields that were not set by the API will not be included,
|
||||
and keys will match the API response, *not* the property names from the model.
|
||||
|
||||
For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property,
|
||||
the output will use the `"fooBar"` key (unless `use_api_names=False` is passed).
|
||||
|
||||
Args:
|
||||
indent: Indentation to use in the JSON output. If `None` is passed, the output will be compact. Defaults to `2`
|
||||
use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`.
|
||||
exclude_unset: Whether to exclude fields that have not been explicitly set.
|
||||
exclude_defaults: Whether to exclude fields that have the default value.
|
||||
exclude_none: Whether to exclude fields that have a value of `None`.
|
||||
warnings: Whether to show any warnings that occurred during serialization. This is only supported in Pydantic v2.
|
||||
"""
|
||||
return self.model_dump_json(
|
||||
indent=indent,
|
||||
by_alias=use_api_names,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
# mypy complains about an invalid self arg
|
||||
return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc]
|
||||
|
||||
# Override the 'construct' method in a way that supports recursive parsing without validation.
|
||||
# Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836.
|
||||
@classmethod
|
||||
@override
|
||||
def construct( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
__cls: Type[ModelT],
|
||||
_fields_set: set[str] | None = None,
|
||||
**values: object,
|
||||
) -> ModelT:
|
||||
m = __cls.__new__(__cls)
|
||||
fields_values: dict[str, object] = {}
|
||||
|
||||
config = get_model_config(__cls)
|
||||
populate_by_name = (
|
||||
config.allow_population_by_field_name
|
||||
if isinstance(config, _ConfigProtocol)
|
||||
else config.get("populate_by_name")
|
||||
)
|
||||
|
||||
if _fields_set is None:
|
||||
_fields_set = set()
|
||||
|
||||
model_fields = get_model_fields(__cls)
|
||||
for name, field in model_fields.items():
|
||||
key = field.alias
|
||||
if key is None or (key not in values and populate_by_name):
|
||||
key = name
|
||||
|
||||
if key in values:
|
||||
fields_values[name] = _construct_field(value=values[key], field=field, key=key)
|
||||
_fields_set.add(name)
|
||||
else:
|
||||
fields_values[name] = field_get_default(field)
|
||||
|
||||
extra_field_type = _get_extra_fields_type(__cls)
|
||||
|
||||
_extra = {}
|
||||
for key, value in values.items():
|
||||
if key not in model_fields:
|
||||
parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value
|
||||
|
||||
if PYDANTIC_V1:
|
||||
_fields_set.add(key)
|
||||
fields_values[key] = parsed
|
||||
else:
|
||||
_extra[key] = parsed
|
||||
|
||||
object.__setattr__(m, "__dict__", fields_values)
|
||||
|
||||
if PYDANTIC_V1:
|
||||
# init_private_attributes() does not exist in v2
|
||||
m._init_private_attributes() # type: ignore
|
||||
|
||||
# copied from Pydantic v1's `construct()` method
|
||||
object.__setattr__(m, "__fields_set__", _fields_set)
|
||||
else:
|
||||
# these properties are copied from Pydantic's `model_construct()` method
|
||||
object.__setattr__(m, "__pydantic_private__", None)
|
||||
object.__setattr__(m, "__pydantic_extra__", _extra)
|
||||
object.__setattr__(m, "__pydantic_fields_set__", _fields_set)
|
||||
|
||||
return m
|
||||
|
||||
if not TYPE_CHECKING:
|
||||
# type checkers incorrectly complain about this assignment
|
||||
# because the type signatures are technically different
|
||||
# although not in practice
|
||||
model_construct = construct
|
||||
|
||||
if PYDANTIC_V1:
|
||||
# we define aliases for some of the new pydantic v2 methods so
|
||||
# that we can just document these methods without having to specify
|
||||
# a specific pydantic version as some users may not know which
|
||||
# pydantic version they are currently using
|
||||
|
||||
@override
|
||||
def model_dump(
|
||||
self,
|
||||
*,
|
||||
mode: Literal["json", "python"] | str = "python",
|
||||
include: IncEx | None = None,
|
||||
exclude: IncEx | None = None,
|
||||
context: Any | None = None,
|
||||
by_alias: bool | None = None,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
exclude_none: bool = False,
|
||||
exclude_computed_fields: bool = False,
|
||||
round_trip: bool = False,
|
||||
warnings: bool | Literal["none", "warn", "error"] = True,
|
||||
fallback: Callable[[Any], Any] | None = None,
|
||||
serialize_as_any: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump
|
||||
|
||||
Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.
|
||||
|
||||
Args:
|
||||
mode: The mode in which `to_python` should run.
|
||||
If mode is 'json', the output will only contain JSON serializable types.
|
||||
If mode is 'python', the output may contain non-JSON-serializable Python objects.
|
||||
include: A set of fields to include in the output.
|
||||
exclude: A set of fields to exclude from the output.
|
||||
context: Additional context to pass to the serializer.
|
||||
by_alias: Whether to use the field's alias in the dictionary key if defined.
|
||||
exclude_unset: Whether to exclude fields that have not been explicitly set.
|
||||
exclude_defaults: Whether to exclude fields that are set to their default value.
|
||||
exclude_none: Whether to exclude fields that have a value of `None`.
|
||||
exclude_computed_fields: Whether to exclude computed fields.
|
||||
While this can be useful for round-tripping, it is usually recommended to use the dedicated
|
||||
`round_trip` parameter instead.
|
||||
round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].
|
||||
warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,
|
||||
"error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
|
||||
fallback: A function to call when an unknown value is encountered. If not provided,
|
||||
a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
|
||||
serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
|
||||
|
||||
Returns:
|
||||
A dictionary representation of the model.
|
||||
"""
|
||||
if mode not in {"json", "python"}:
|
||||
raise ValueError("mode must be either 'json' or 'python'")
|
||||
if round_trip != False:
|
||||
raise ValueError("round_trip is only supported in Pydantic v2")
|
||||
if warnings != True:
|
||||
raise ValueError("warnings is only supported in Pydantic v2")
|
||||
if context is not None:
|
||||
raise ValueError("context is only supported in Pydantic v2")
|
||||
if serialize_as_any != False:
|
||||
raise ValueError("serialize_as_any is only supported in Pydantic v2")
|
||||
if fallback is not None:
|
||||
raise ValueError("fallback is only supported in Pydantic v2")
|
||||
if exclude_computed_fields != False:
|
||||
raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
|
||||
dumped = super().dict( # pyright: ignore[reportDeprecated]
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias if by_alias is not None else False,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
|
||||
return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped
|
||||
|
||||
@override
|
||||
def model_dump_json(
|
||||
self,
|
||||
*,
|
||||
indent: int | None = None,
|
||||
ensure_ascii: bool = False,
|
||||
include: IncEx | None = None,
|
||||
exclude: IncEx | None = None,
|
||||
context: Any | None = None,
|
||||
by_alias: bool | None = None,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
exclude_none: bool = False,
|
||||
exclude_computed_fields: bool = False,
|
||||
round_trip: bool = False,
|
||||
warnings: bool | Literal["none", "warn", "error"] = True,
|
||||
fallback: Callable[[Any], Any] | None = None,
|
||||
serialize_as_any: bool = False,
|
||||
) -> str:
|
||||
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json
|
||||
|
||||
Generates a JSON representation of the model using Pydantic's `to_json` method.
|
||||
|
||||
Args:
|
||||
indent: Indentation to use in the JSON output. If None is passed, the output will be compact.
|
||||
include: Field(s) to include in the JSON output. Can take either a string or set of strings.
|
||||
exclude: Field(s) to exclude from the JSON output. Can take either a string or set of strings.
|
||||
by_alias: Whether to serialize using field aliases.
|
||||
exclude_unset: Whether to exclude fields that have not been explicitly set.
|
||||
exclude_defaults: Whether to exclude fields that have the default value.
|
||||
exclude_none: Whether to exclude fields that have a value of `None`.
|
||||
round_trip: Whether to use serialization/deserialization between JSON and class instance.
|
||||
warnings: Whether to show any warnings that occurred during serialization.
|
||||
|
||||
Returns:
|
||||
A JSON string representation of the model.
|
||||
"""
|
||||
if round_trip != False:
|
||||
raise ValueError("round_trip is only supported in Pydantic v2")
|
||||
if warnings != True:
|
||||
raise ValueError("warnings is only supported in Pydantic v2")
|
||||
if context is not None:
|
||||
raise ValueError("context is only supported in Pydantic v2")
|
||||
if serialize_as_any != False:
|
||||
raise ValueError("serialize_as_any is only supported in Pydantic v2")
|
||||
if fallback is not None:
|
||||
raise ValueError("fallback is only supported in Pydantic v2")
|
||||
if ensure_ascii != False:
|
||||
raise ValueError("ensure_ascii is only supported in Pydantic v2")
|
||||
if exclude_computed_fields != False:
|
||||
raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
|
||||
return super().json( # type: ignore[reportDeprecated]
|
||||
indent=indent,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias if by_alias is not None else False,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
|
||||
|
||||
def _construct_field(value: object, field: FieldInfo, key: str) -> object:
|
||||
if value is None:
|
||||
return field_get_default(field)
|
||||
|
||||
if PYDANTIC_V1:
|
||||
type_ = cast(type, field.outer_type_) # type: ignore
|
||||
else:
|
||||
type_ = field.annotation # type: ignore
|
||||
|
||||
if type_ is None:
|
||||
raise RuntimeError(f"Unexpected field type is None for {key}")
|
||||
|
||||
return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None))
|
||||
|
||||
|
||||
def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None:
|
||||
if PYDANTIC_V1:
|
||||
# TODO
|
||||
return None
|
||||
|
||||
schema = cls.__pydantic_core_schema__
|
||||
if schema["type"] == "model":
|
||||
fields = schema["schema"]
|
||||
if fields["type"] == "model-fields":
|
||||
extras = fields.get("extras_schema")
|
||||
if extras and "cls" in extras:
|
||||
# mypy can't narrow the type
|
||||
return extras["cls"] # type: ignore[no-any-return]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def is_basemodel(type_: type) -> bool:
|
||||
"""Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`"""
|
||||
if is_union(type_):
|
||||
for variant in get_args(type_):
|
||||
if is_basemodel(variant):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
return is_basemodel_type(type_)
|
||||
|
||||
|
||||
def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]:
|
||||
origin = get_origin(type_) or type_
|
||||
if not inspect.isclass(origin):
|
||||
return False
|
||||
return issubclass(origin, BaseModel) or issubclass(origin, GenericModel)
|
||||
|
||||
|
||||
def build(
|
||||
base_model_cls: Callable[P, _BaseModelT],
|
||||
*args: P.args,
|
||||
**kwargs: P.kwargs,
|
||||
) -> _BaseModelT:
|
||||
"""Construct a BaseModel class without validation.
|
||||
|
||||
This is useful for cases where you need to instantiate a `BaseModel`
|
||||
from an API response as this provides type-safe params which isn't supported
|
||||
by helpers like `construct_type()`.
|
||||
|
||||
```py
|
||||
build(MyModel, my_field_a="foo", my_field_b=123)
|
||||
```
|
||||
"""
|
||||
if args:
|
||||
raise TypeError(
|
||||
"Received positional arguments which are not supported; Keyword arguments must be used instead",
|
||||
)
|
||||
|
||||
return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs))
|
||||
|
||||
|
||||
def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T:
|
||||
"""Loose coercion to the expected type with construction of nested values.
|
||||
|
||||
Note: the returned value from this function is not guaranteed to match the
|
||||
given type.
|
||||
"""
|
||||
return cast(_T, construct_type(value=value, type_=type_))
|
||||
|
||||
|
||||
def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object:
|
||||
"""Loose coercion to the expected type with construction of nested values.
|
||||
|
||||
If the given value does not match the expected type then it is returned as-is.
|
||||
"""
|
||||
|
||||
# store a reference to the original type we were given before we extract any inner
|
||||
# types so that we can properly resolve forward references in `TypeAliasType` annotations
|
||||
original_type = None
|
||||
|
||||
# we allow `object` as the input type because otherwise, passing things like
|
||||
# `Literal['value']` will be reported as a type error by type checkers
|
||||
type_ = cast("type[object]", type_)
|
||||
if is_type_alias_type(type_):
|
||||
original_type = type_ # type: ignore[unreachable]
|
||||
type_ = type_.__value__ # type: ignore[unreachable]
|
||||
|
||||
# unwrap `Annotated[T, ...]` -> `T`
|
||||
if metadata is not None and len(metadata) > 0:
|
||||
meta: tuple[Any, ...] = tuple(metadata)
|
||||
elif is_annotated_type(type_):
|
||||
meta = get_args(type_)[1:]
|
||||
type_ = extract_type_arg(type_, 0)
|
||||
else:
|
||||
meta = tuple()
|
||||
|
||||
# we need to use the origin class for any types that are subscripted generics
|
||||
# e.g. Dict[str, object]
|
||||
origin = get_origin(type_) or type_
|
||||
args = get_args(type_)
|
||||
|
||||
if is_union(origin):
|
||||
try:
|
||||
return validate_type(type_=cast("type[object]", original_type or type_), value=value)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# if the type is a discriminated union then we want to construct the right variant
|
||||
# in the union, even if the data doesn't match exactly, otherwise we'd break code
|
||||
# that relies on the constructed class types, e.g.
|
||||
#
|
||||
# class FooType:
|
||||
# kind: Literal['foo']
|
||||
# value: str
|
||||
#
|
||||
# class BarType:
|
||||
# kind: Literal['bar']
|
||||
# value: int
|
||||
#
|
||||
# without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then
|
||||
# we'd end up constructing `FooType` when it should be `BarType`.
|
||||
discriminator = _build_discriminated_union_meta(union=type_, meta_annotations=meta)
|
||||
if discriminator and is_mapping(value):
|
||||
variant_value = value.get(discriminator.field_alias_from or discriminator.field_name)
|
||||
if variant_value and isinstance(variant_value, str):
|
||||
variant_type = discriminator.mapping.get(variant_value)
|
||||
if variant_type:
|
||||
return construct_type(type_=variant_type, value=value)
|
||||
|
||||
# if the data is not valid, use the first variant that doesn't fail while deserializing
|
||||
for variant in args:
|
||||
try:
|
||||
return construct_type(value=value, type_=variant)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
raise RuntimeError(f"Could not convert data into a valid instance of {type_}")
|
||||
|
||||
if origin == dict:
|
||||
if not is_mapping(value):
|
||||
return value
|
||||
|
||||
_, items_type = get_args(type_) # Dict[_, items_type]
|
||||
return {key: construct_type(value=item, type_=items_type) for key, item in value.items()}
|
||||
|
||||
if (
|
||||
not is_literal_type(type_)
|
||||
and inspect.isclass(origin)
|
||||
and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel))
|
||||
):
|
||||
if is_list(value):
|
||||
return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value]
|
||||
|
||||
if is_mapping(value):
|
||||
if issubclass(type_, BaseModel):
|
||||
return type_.construct(**value) # type: ignore[arg-type]
|
||||
|
||||
return cast(Any, type_).construct(**value)
|
||||
|
||||
if origin == list:
|
||||
if not is_list(value):
|
||||
return value
|
||||
|
||||
inner_type = args[0] # List[inner_type]
|
||||
return [construct_type(value=entry, type_=inner_type) for entry in value]
|
||||
|
||||
if origin == float:
|
||||
if isinstance(value, int):
|
||||
coerced = float(value)
|
||||
if coerced != value:
|
||||
return value
|
||||
return coerced
|
||||
|
||||
return value
|
||||
|
||||
if type_ == datetime:
|
||||
try:
|
||||
return parse_datetime(value) # type: ignore
|
||||
except Exception:
|
||||
return value
|
||||
|
||||
if type_ == date:
|
||||
try:
|
||||
return parse_date(value) # type: ignore
|
||||
except Exception:
|
||||
return value
|
||||
|
||||
return value
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class CachedDiscriminatorType(Protocol):
|
||||
__discriminator__: DiscriminatorDetails
|
||||
|
||||
|
||||
DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary()
|
||||
|
||||
|
||||
class DiscriminatorDetails:
|
||||
field_name: str
|
||||
"""The name of the discriminator field in the variant class, e.g.
|
||||
|
||||
```py
|
||||
class Foo(BaseModel):
|
||||
type: Literal['foo']
|
||||
```
|
||||
|
||||
Will result in field_name='type'
|
||||
"""
|
||||
|
||||
field_alias_from: str | None
|
||||
"""The name of the discriminator field in the API response, e.g.
|
||||
|
||||
```py
|
||||
class Foo(BaseModel):
|
||||
type: Literal['foo'] = Field(alias='type_from_api')
|
||||
```
|
||||
|
||||
Will result in field_alias_from='type_from_api'
|
||||
"""
|
||||
|
||||
mapping: dict[str, type]
|
||||
"""Mapping of discriminator value to variant type, e.g.
|
||||
|
||||
{'foo': FooVariant, 'bar': BarVariant}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
mapping: dict[str, type],
|
||||
discriminator_field: str,
|
||||
discriminator_alias: str | None,
|
||||
) -> None:
|
||||
self.mapping = mapping
|
||||
self.field_name = discriminator_field
|
||||
self.field_alias_from = discriminator_alias
|
||||
|
||||
|
||||
def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None:
|
||||
cached = DISCRIMINATOR_CACHE.get(union)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
discriminator_field_name: str | None = None
|
||||
|
||||
for annotation in meta_annotations:
|
||||
if isinstance(annotation, PropertyInfo) and annotation.discriminator is not None:
|
||||
discriminator_field_name = annotation.discriminator
|
||||
break
|
||||
|
||||
if not discriminator_field_name:
|
||||
return None
|
||||
|
||||
mapping: dict[str, type] = {}
|
||||
discriminator_alias: str | None = None
|
||||
|
||||
for variant in get_args(union):
|
||||
variant = strip_annotated_type(variant)
|
||||
if is_basemodel_type(variant):
|
||||
if PYDANTIC_V1:
|
||||
field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
|
||||
if not field_info:
|
||||
continue
|
||||
|
||||
# Note: if one variant defines an alias then they all should
|
||||
discriminator_alias = field_info.alias
|
||||
|
||||
if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation):
|
||||
for entry in get_args(annotation):
|
||||
if isinstance(entry, str):
|
||||
mapping[entry] = variant
|
||||
else:
|
||||
field = _extract_field_schema_pv2(variant, discriminator_field_name)
|
||||
if not field:
|
||||
continue
|
||||
|
||||
# Note: if one variant defines an alias then they all should
|
||||
discriminator_alias = field.get("serialization_alias")
|
||||
|
||||
field_schema = field["schema"]
|
||||
|
||||
if field_schema["type"] == "literal":
|
||||
for entry in cast("LiteralSchema", field_schema)["expected"]:
|
||||
if isinstance(entry, str):
|
||||
mapping[entry] = variant
|
||||
|
||||
if not mapping:
|
||||
return None
|
||||
|
||||
details = DiscriminatorDetails(
|
||||
mapping=mapping,
|
||||
discriminator_field=discriminator_field_name,
|
||||
discriminator_alias=discriminator_alias,
|
||||
)
|
||||
DISCRIMINATOR_CACHE.setdefault(union, details)
|
||||
return details
|
||||
|
||||
|
||||
def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None:
|
||||
schema = model.__pydantic_core_schema__
|
||||
if schema["type"] == "definitions":
|
||||
schema = schema["schema"]
|
||||
|
||||
if schema["type"] != "model":
|
||||
return None
|
||||
|
||||
schema = cast("ModelSchema", schema)
|
||||
fields_schema = schema["schema"]
|
||||
if fields_schema["type"] != "model-fields":
|
||||
return None
|
||||
|
||||
fields_schema = cast("ModelFieldsSchema", fields_schema)
|
||||
field = fields_schema["fields"].get(field_name)
|
||||
if not field:
|
||||
return None
|
||||
|
||||
return cast("ModelField", field) # pyright: ignore[reportUnnecessaryCast]
|
||||
|
||||
|
||||
def validate_type(*, type_: type[_T], value: object) -> _T:
|
||||
"""Strict validation that the given value matches the expected type"""
|
||||
if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel):
|
||||
return cast(_T, parse_obj(type_, value))
|
||||
|
||||
return cast(_T, _validate_non_model_type(type_=type_, value=value))
|
||||
|
||||
|
||||
def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None:
|
||||
"""Add a pydantic config for the given type.
|
||||
|
||||
Note: this is a no-op on Pydantic v1.
|
||||
"""
|
||||
setattr(typ, "__pydantic_config__", config) # noqa: B010
|
||||
|
||||
|
||||
# our use of subclassing here causes weirdness for type checkers,
|
||||
# so we just pretend that we don't subclass
|
||||
if TYPE_CHECKING:
|
||||
GenericModel = BaseModel
|
||||
else:
|
||||
|
||||
class GenericModel(BaseGenericModel, BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
if not PYDANTIC_V1:
|
||||
from pydantic import TypeAdapter as _TypeAdapter
|
||||
|
||||
_CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter))
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydantic import TypeAdapter
|
||||
else:
|
||||
TypeAdapter = _CachedTypeAdapter
|
||||
|
||||
def _validate_non_model_type(*, type_: type[_T], value: object) -> _T:
|
||||
return TypeAdapter(type_).validate_python(value)
|
||||
|
||||
elif not TYPE_CHECKING: # TODO: condition is weird
|
||||
|
||||
class RootModel(GenericModel, Generic[_T]):
|
||||
"""Used as a placeholder to easily convert runtime types to a Pydantic format
|
||||
to provide validation.
|
||||
|
||||
For example:
|
||||
```py
|
||||
validated = RootModel[int](__root__="5").__root__
|
||||
# validated: 5
|
||||
```
|
||||
"""
|
||||
|
||||
__root__: _T
|
||||
|
||||
def _validate_non_model_type(*, type_: type[_T], value: object) -> _T:
|
||||
model = _create_pydantic_model(type_).validate(value)
|
||||
return cast(_T, model.__root__)
|
||||
|
||||
def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]:
|
||||
return RootModel[type_] # type: ignore
|
||||
|
||||
|
||||
class FinalRequestOptionsInput(TypedDict, total=False):
|
||||
method: Required[str]
|
||||
url: Required[str]
|
||||
params: Query
|
||||
headers: Headers
|
||||
max_retries: int
|
||||
timeout: float | Timeout | None
|
||||
files: HttpxRequestFiles | None
|
||||
idempotency_key: str
|
||||
content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None]
|
||||
json_data: Body
|
||||
extra_json: AnyMapping
|
||||
follow_redirects: bool
|
||||
|
||||
|
||||
@final
|
||||
class FinalRequestOptions(pydantic.BaseModel):
|
||||
method: str
|
||||
url: str
|
||||
params: Query = {}
|
||||
headers: Union[Headers, NotGiven] = NotGiven()
|
||||
max_retries: Union[int, NotGiven] = NotGiven()
|
||||
timeout: Union[float, Timeout, None, NotGiven] = NotGiven()
|
||||
files: Union[HttpxRequestFiles, None] = None
|
||||
idempotency_key: Union[str, None] = None
|
||||
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
|
||||
follow_redirects: Union[bool, None] = None
|
||||
|
||||
content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None
|
||||
# It should be noted that we cannot use `json` here as that would override
|
||||
# a BaseModel method in an incompatible fashion.
|
||||
json_data: Union[Body, None] = None
|
||||
extra_json: Union[AnyMapping, None] = None
|
||||
|
||||
if PYDANTIC_V1:
|
||||
|
||||
class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
|
||||
arbitrary_types_allowed: bool = True
|
||||
else:
|
||||
model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
def get_max_retries(self, max_retries: int) -> int:
|
||||
if isinstance(self.max_retries, NotGiven):
|
||||
return max_retries
|
||||
return self.max_retries
|
||||
|
||||
def _strip_raw_response_header(self) -> None:
|
||||
if not is_given(self.headers):
|
||||
return
|
||||
|
||||
if self.headers.get(RAW_RESPONSE_HEADER):
|
||||
self.headers = {**self.headers}
|
||||
self.headers.pop(RAW_RESPONSE_HEADER)
|
||||
|
||||
# override the `construct` method so that we can run custom transformations.
|
||||
# this is necessary as we don't want to do any actual runtime type checking
|
||||
# (which means we can't use validators) but we do want to ensure that `NotGiven`
|
||||
# values are not present
|
||||
#
|
||||
# type ignore required because we're adding explicit types to `**values`
|
||||
@classmethod
|
||||
def construct( # type: ignore
|
||||
cls,
|
||||
_fields_set: set[str] | None = None,
|
||||
**values: Unpack[FinalRequestOptionsInput],
|
||||
) -> FinalRequestOptions:
|
||||
kwargs: dict[str, Any] = {
|
||||
# we unconditionally call `strip_not_given` on any value
|
||||
# as it will just ignore any non-mapping types
|
||||
key: strip_not_given(value)
|
||||
for key, value in values.items()
|
||||
}
|
||||
if PYDANTIC_V1:
|
||||
return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated]
|
||||
return super().model_construct(_fields_set, **kwargs)
|
||||
|
||||
if not TYPE_CHECKING:
|
||||
# type checkers incorrectly complain about this assignment
|
||||
model_construct = construct
|
||||
@@ -0,0 +1,168 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, List, Tuple, Union, Mapping, TypeVar
|
||||
from urllib.parse import parse_qs, urlencode
|
||||
from typing_extensions import Literal, get_args
|
||||
|
||||
from ._types import NotGiven, not_given
|
||||
from ._utils import flatten
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
|
||||
NestedFormat = Literal["dots", "brackets"]
|
||||
|
||||
PrimitiveData = Union[str, int, float, bool, None]
|
||||
# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"]
|
||||
# https://github.com/microsoft/pyright/issues/3555
|
||||
Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"]
|
||||
Params = Mapping[str, Data]
|
||||
|
||||
|
||||
class Querystring:
|
||||
array_format: ArrayFormat
|
||||
nested_format: NestedFormat
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
array_format: ArrayFormat = "repeat",
|
||||
nested_format: NestedFormat = "brackets",
|
||||
) -> None:
|
||||
self.array_format = array_format
|
||||
self.nested_format = nested_format
|
||||
|
||||
def parse(self, query: str) -> Mapping[str, object]:
|
||||
# Note: custom format syntax is not supported yet
|
||||
return parse_qs(query)
|
||||
|
||||
def stringify(
|
||||
self,
|
||||
params: Params,
|
||||
*,
|
||||
array_format: ArrayFormat | NotGiven = not_given,
|
||||
nested_format: NestedFormat | NotGiven = not_given,
|
||||
) -> str:
|
||||
return urlencode(
|
||||
self.stringify_items(
|
||||
params,
|
||||
array_format=array_format,
|
||||
nested_format=nested_format,
|
||||
)
|
||||
)
|
||||
|
||||
def stringify_items(
|
||||
self,
|
||||
params: Params,
|
||||
*,
|
||||
array_format: ArrayFormat | NotGiven = not_given,
|
||||
nested_format: NestedFormat | NotGiven = not_given,
|
||||
) -> list[tuple[str, str]]:
|
||||
opts = Options(
|
||||
qs=self,
|
||||
array_format=array_format,
|
||||
nested_format=nested_format,
|
||||
)
|
||||
return flatten([self._stringify_item(key, value, opts) for key, value in params.items()])
|
||||
|
||||
def _stringify_item(
|
||||
self,
|
||||
key: str,
|
||||
value: Data,
|
||||
opts: Options,
|
||||
) -> list[tuple[str, str]]:
|
||||
if isinstance(value, Mapping):
|
||||
items: list[tuple[str, str]] = []
|
||||
nested_format = opts.nested_format
|
||||
for subkey, subvalue in value.items():
|
||||
items.extend(
|
||||
self._stringify_item(
|
||||
# TODO: error if unknown format
|
||||
f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]",
|
||||
subvalue,
|
||||
opts,
|
||||
)
|
||||
)
|
||||
return items
|
||||
|
||||
if isinstance(value, (list, tuple)):
|
||||
array_format = opts.array_format
|
||||
if array_format == "comma":
|
||||
return [
|
||||
(
|
||||
key,
|
||||
",".join(self._primitive_value_to_str(item) for item in value if item is not None),
|
||||
),
|
||||
]
|
||||
elif array_format == "repeat":
|
||||
items = []
|
||||
for item in value:
|
||||
items.extend(self._stringify_item(key, item, opts))
|
||||
return items
|
||||
elif array_format == "indices":
|
||||
items = []
|
||||
for i, item in enumerate(value):
|
||||
items.extend(self._stringify_item(f"{key}[{i}]", item, opts))
|
||||
return items
|
||||
elif array_format == "brackets":
|
||||
items = []
|
||||
key = key + "[]"
|
||||
for item in value:
|
||||
items.extend(self._stringify_item(key, item, opts))
|
||||
return items
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}"
|
||||
)
|
||||
|
||||
serialised = self._primitive_value_to_str(value)
|
||||
if not serialised:
|
||||
return []
|
||||
return [(key, serialised)]
|
||||
|
||||
def _primitive_value_to_str(self, value: PrimitiveData) -> str:
|
||||
# copied from httpx
|
||||
if value is True:
|
||||
return "true"
|
||||
elif value is False:
|
||||
return "false"
|
||||
elif value is None:
|
||||
return ""
|
||||
return str(value)
|
||||
|
||||
|
||||
_qs = Querystring()
|
||||
parse = _qs.parse
|
||||
stringify = _qs.stringify
|
||||
stringify_items = _qs.stringify_items
|
||||
|
||||
|
||||
class Options:
|
||||
array_format: ArrayFormat
|
||||
nested_format: NestedFormat
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
qs: Querystring = _qs,
|
||||
*,
|
||||
array_format: ArrayFormat | NotGiven = not_given,
|
||||
nested_format: NestedFormat | NotGiven = not_given,
|
||||
) -> None:
|
||||
self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format
|
||||
self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format
|
||||
@@ -0,0 +1,58 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import anyio
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._client import GeminiNextGenAPIClient, AsyncGeminiNextGenAPIClient
|
||||
|
||||
|
||||
class SyncAPIResource:
|
||||
_client: GeminiNextGenAPIClient
|
||||
|
||||
def __init__(self, client: GeminiNextGenAPIClient) -> None:
|
||||
self._client = client
|
||||
self._get = client.get
|
||||
self._post = client.post
|
||||
self._patch = client.patch
|
||||
self._put = client.put
|
||||
self._delete = client.delete
|
||||
self._get_api_list = client.get_api_list
|
||||
|
||||
def _sleep(self, seconds: float) -> None:
|
||||
time.sleep(seconds)
|
||||
|
||||
|
||||
class AsyncAPIResource:
|
||||
_client: AsyncGeminiNextGenAPIClient
|
||||
|
||||
def __init__(self, client: AsyncGeminiNextGenAPIClient) -> None:
|
||||
self._client = client
|
||||
self._get = client.get
|
||||
self._post = client.post
|
||||
self._patch = client.patch
|
||||
self._put = client.put
|
||||
self._delete = client.delete
|
||||
self._get_api_list = client.get_api_list
|
||||
|
||||
async def _sleep(self, seconds: float) -> None:
|
||||
await anyio.sleep(seconds)
|
||||
@@ -0,0 +1,850 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import inspect
|
||||
import logging
|
||||
import datetime
|
||||
import functools
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Union,
|
||||
Generic,
|
||||
TypeVar,
|
||||
Callable,
|
||||
Iterator,
|
||||
AsyncIterator,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
from typing_extensions import Awaitable, ParamSpec, override, get_origin
|
||||
|
||||
import anyio
|
||||
import httpx
|
||||
import pydantic
|
||||
|
||||
from ._types import NoneType
|
||||
from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base
|
||||
from ._models import BaseModel, is_basemodel
|
||||
from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER
|
||||
from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type
|
||||
from ._exceptions import APIResponseValidationError, GeminiNextGenAPIClientError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._models import FinalRequestOptions
|
||||
from ._base_client import BaseClient
|
||||
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
_T = TypeVar("_T")
|
||||
_APIResponseT = TypeVar("_APIResponseT", bound="APIResponse[Any]")
|
||||
_AsyncAPIResponseT = TypeVar("_AsyncAPIResponseT", bound="AsyncAPIResponse[Any]")
|
||||
|
||||
log: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseAPIResponse(Generic[R]):
|
||||
_cast_to: type[R]
|
||||
_client: BaseClient[Any, Any]
|
||||
_parsed_by_type: dict[type[Any], Any]
|
||||
_is_sse_stream: bool
|
||||
_stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None
|
||||
_options: FinalRequestOptions
|
||||
|
||||
http_response: httpx.Response
|
||||
|
||||
retries_taken: int
|
||||
"""The number of retries made. If no retries happened this will be `0`"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
raw: httpx.Response,
|
||||
cast_to: type[R],
|
||||
client: BaseClient[Any, Any],
|
||||
stream: bool,
|
||||
stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
|
||||
options: FinalRequestOptions,
|
||||
retries_taken: int = 0,
|
||||
) -> None:
|
||||
self._cast_to = cast_to
|
||||
self._client = client
|
||||
self._parsed_by_type = {}
|
||||
self._is_sse_stream = stream
|
||||
self._stream_cls = stream_cls
|
||||
self._options = options
|
||||
self.http_response = raw
|
||||
self.retries_taken = retries_taken
|
||||
|
||||
@property
|
||||
def headers(self) -> httpx.Headers:
|
||||
return self.http_response.headers
|
||||
|
||||
@property
|
||||
def http_request(self) -> httpx.Request:
|
||||
"""Returns the httpx Request instance associated with the current response."""
|
||||
return self.http_response.request
|
||||
|
||||
@property
|
||||
def status_code(self) -> int:
|
||||
return self.http_response.status_code
|
||||
|
||||
@property
|
||||
def url(self) -> httpx.URL:
|
||||
"""Returns the URL for which the request was made."""
|
||||
return self.http_response.url
|
||||
|
||||
@property
|
||||
def method(self) -> str:
|
||||
return self.http_request.method
|
||||
|
||||
@property
|
||||
def http_version(self) -> str:
|
||||
return self.http_response.http_version
|
||||
|
||||
@property
|
||||
def elapsed(self) -> datetime.timedelta:
|
||||
"""The time taken for the complete request/response cycle to complete."""
|
||||
return self.http_response.elapsed
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool:
|
||||
"""Whether or not the response body has been closed.
|
||||
|
||||
If this is False then there is response data that has not been read yet.
|
||||
You must either fully consume the response body or call `.close()`
|
||||
before discarding the response to prevent resource leaks.
|
||||
"""
|
||||
return self.http_response.is_closed
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<{self.__class__.__name__} [{self.status_code} {self.http_response.reason_phrase}] type={self._cast_to}>"
|
||||
)
|
||||
|
||||
def _parse(self, *, to: type[_T] | None = None) -> R | _T:
|
||||
cast_to = to if to is not None else self._cast_to
|
||||
|
||||
# unwrap `TypeAlias('Name', T)` -> `T`
|
||||
if is_type_alias_type(cast_to):
|
||||
cast_to = cast_to.__value__ # type: ignore[unreachable]
|
||||
|
||||
# unwrap `Annotated[T, ...]` -> `T`
|
||||
if cast_to and is_annotated_type(cast_to):
|
||||
cast_to = extract_type_arg(cast_to, 0)
|
||||
|
||||
origin = get_origin(cast_to) or cast_to
|
||||
|
||||
if self._is_sse_stream:
|
||||
if to:
|
||||
if not is_stream_class_type(to):
|
||||
raise TypeError(f"Expected custom parse type to be a subclass of {Stream} or {AsyncStream}")
|
||||
|
||||
return cast(
|
||||
_T,
|
||||
to(
|
||||
cast_to=extract_stream_chunk_type(
|
||||
to,
|
||||
failure_message="Expected custom stream type to be passed with a type argument, e.g. Stream[ChunkType]",
|
||||
),
|
||||
response=self.http_response,
|
||||
client=cast(Any, self._client),
|
||||
options=self._options,
|
||||
),
|
||||
)
|
||||
|
||||
if self._stream_cls:
|
||||
return cast(
|
||||
R,
|
||||
self._stream_cls(
|
||||
cast_to=extract_stream_chunk_type(self._stream_cls),
|
||||
response=self.http_response,
|
||||
client=cast(Any, self._client),
|
||||
options=self._options,
|
||||
),
|
||||
)
|
||||
|
||||
stream_cls = cast("type[Stream[Any]] | type[AsyncStream[Any]] | None", self._client._default_stream_cls)
|
||||
if stream_cls is None:
|
||||
raise MissingStreamClassError()
|
||||
|
||||
return cast(
|
||||
R,
|
||||
stream_cls(
|
||||
cast_to=cast_to,
|
||||
response=self.http_response,
|
||||
client=cast(Any, self._client),
|
||||
options=self._options,
|
||||
),
|
||||
)
|
||||
|
||||
if cast_to is NoneType:
|
||||
return cast(R, None)
|
||||
|
||||
response = self.http_response
|
||||
if cast_to == str:
|
||||
return cast(R, response.text)
|
||||
|
||||
if cast_to == bytes:
|
||||
return cast(R, response.content)
|
||||
|
||||
if cast_to == int:
|
||||
return cast(R, int(response.text))
|
||||
|
||||
if cast_to == float:
|
||||
return cast(R, float(response.text))
|
||||
|
||||
if cast_to == bool:
|
||||
return cast(R, response.text.lower() == "true")
|
||||
|
||||
if origin == APIResponse:
|
||||
raise RuntimeError("Unexpected state - cast_to is `APIResponse`")
|
||||
|
||||
if inspect.isclass(origin) and issubclass(origin, httpx.Response):
|
||||
# Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response
|
||||
# and pass that class to our request functions. We cannot change the variance to be either
|
||||
# covariant or contravariant as that makes our usage of ResponseT illegal. We could construct
|
||||
# the response class ourselves but that is something that should be supported directly in httpx
|
||||
# as it would be easy to incorrectly construct the Response object due to the multitude of arguments.
|
||||
if cast_to != httpx.Response:
|
||||
raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`")
|
||||
return cast(R, response)
|
||||
|
||||
if (
|
||||
inspect.isclass(
|
||||
origin # pyright: ignore[reportUnknownArgumentType]
|
||||
)
|
||||
and not issubclass(origin, BaseModel)
|
||||
and issubclass(origin, pydantic.BaseModel)
|
||||
):
|
||||
raise TypeError(
|
||||
"Pydantic models must subclass our base model type, e.g. `from google.genai._interactions import BaseModel`"
|
||||
)
|
||||
|
||||
if (
|
||||
cast_to is not object
|
||||
and not origin is list
|
||||
and not origin is dict
|
||||
and not origin is Union
|
||||
and not issubclass(origin, BaseModel)
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"Unsupported type, expected {cast_to} to be a subclass of {BaseModel}, {dict}, {list}, {Union}, {NoneType}, {str} or {httpx.Response}."
|
||||
)
|
||||
|
||||
# split is required to handle cases where additional information is included
|
||||
# in the response, e.g. application/json; charset=utf-8
|
||||
content_type, *_ = response.headers.get("content-type", "*").split(";")
|
||||
if not content_type.endswith("json"):
|
||||
if is_basemodel(cast_to):
|
||||
try:
|
||||
data = response.json()
|
||||
except Exception as exc:
|
||||
log.debug("Could not read JSON from response data due to %s - %s", type(exc), exc)
|
||||
else:
|
||||
return self._client._process_response_data(
|
||||
data=data,
|
||||
cast_to=cast_to, # type: ignore
|
||||
response=response,
|
||||
)
|
||||
|
||||
if self._client._strict_response_validation:
|
||||
raise APIResponseValidationError(
|
||||
response=response,
|
||||
message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.",
|
||||
body=response.text,
|
||||
)
|
||||
|
||||
# If the API responds with content that isn't JSON then we just return
|
||||
# the (decoded) text without performing any parsing so that you can still
|
||||
# handle the response however you need to.
|
||||
return response.text # type: ignore
|
||||
|
||||
data = response.json()
|
||||
|
||||
return self._client._process_response_data(
|
||||
data=data,
|
||||
cast_to=cast_to, # type: ignore
|
||||
response=response,
|
||||
)
|
||||
|
||||
|
||||
class APIResponse(BaseAPIResponse[R]):
|
||||
@overload
|
||||
def parse(self, *, to: type[_T]) -> _T: ...
|
||||
|
||||
@overload
|
||||
def parse(self) -> R: ...
|
||||
|
||||
def parse(self, *, to: type[_T] | None = None) -> R | _T:
|
||||
"""Returns the rich python representation of this response's data.
|
||||
|
||||
For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`.
|
||||
|
||||
You can customise the type that the response is parsed into through
|
||||
the `to` argument, e.g.
|
||||
|
||||
```py
|
||||
from google.genai._interactions import BaseModel
|
||||
|
||||
|
||||
class MyModel(BaseModel):
|
||||
foo: str
|
||||
|
||||
|
||||
obj = response.parse(to=MyModel)
|
||||
print(obj.foo)
|
||||
```
|
||||
|
||||
We support parsing:
|
||||
- `BaseModel`
|
||||
- `dict`
|
||||
- `list`
|
||||
- `Union`
|
||||
- `str`
|
||||
- `int`
|
||||
- `float`
|
||||
- `httpx.Response`
|
||||
"""
|
||||
cache_key = to if to is not None else self._cast_to
|
||||
cached = self._parsed_by_type.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached # type: ignore[no-any-return]
|
||||
|
||||
if not self._is_sse_stream:
|
||||
self.read()
|
||||
|
||||
parsed = self._parse(to=to)
|
||||
if is_given(self._options.post_parser):
|
||||
parsed = self._options.post_parser(parsed)
|
||||
|
||||
self._parsed_by_type[cache_key] = parsed
|
||||
return parsed
|
||||
|
||||
def read(self) -> bytes:
|
||||
"""Read and return the binary response content."""
|
||||
try:
|
||||
return self.http_response.read()
|
||||
except httpx.StreamConsumed as exc:
|
||||
# The default error raised by httpx isn't very
|
||||
# helpful in our case so we re-raise it with
|
||||
# a different error message.
|
||||
raise StreamAlreadyConsumed() from exc
|
||||
|
||||
def text(self) -> str:
|
||||
"""Read and decode the response content into a string."""
|
||||
self.read()
|
||||
return self.http_response.text
|
||||
|
||||
def json(self) -> object:
|
||||
"""Read and decode the JSON response content."""
|
||||
self.read()
|
||||
return self.http_response.json()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the response and release the connection.
|
||||
|
||||
Automatically called if the response body is read to completion.
|
||||
"""
|
||||
self.http_response.close()
|
||||
|
||||
def iter_bytes(self, chunk_size: int | None = None) -> Iterator[bytes]:
|
||||
"""
|
||||
A byte-iterator over the decoded response content.
|
||||
|
||||
This automatically handles gzip, deflate and brotli encoded responses.
|
||||
"""
|
||||
for chunk in self.http_response.iter_bytes(chunk_size):
|
||||
yield chunk
|
||||
|
||||
def iter_text(self, chunk_size: int | None = None) -> Iterator[str]:
|
||||
"""A str-iterator over the decoded response content
|
||||
that handles both gzip, deflate, etc but also detects the content's
|
||||
string encoding.
|
||||
"""
|
||||
for chunk in self.http_response.iter_text(chunk_size):
|
||||
yield chunk
|
||||
|
||||
def iter_lines(self) -> Iterator[str]:
|
||||
"""Like `iter_text()` but will only yield chunks for each line"""
|
||||
for chunk in self.http_response.iter_lines():
|
||||
yield chunk
|
||||
|
||||
|
||||
class AsyncAPIResponse(BaseAPIResponse[R]):
|
||||
@overload
|
||||
async def parse(self, *, to: type[_T]) -> _T: ...
|
||||
|
||||
@overload
|
||||
async def parse(self) -> R: ...
|
||||
|
||||
async def parse(self, *, to: type[_T] | None = None) -> R | _T:
|
||||
"""Returns the rich python representation of this response's data.
|
||||
|
||||
For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`.
|
||||
|
||||
You can customise the type that the response is parsed into through
|
||||
the `to` argument, e.g.
|
||||
|
||||
```py
|
||||
from google.genai._interactions import BaseModel
|
||||
|
||||
|
||||
class MyModel(BaseModel):
|
||||
foo: str
|
||||
|
||||
|
||||
obj = response.parse(to=MyModel)
|
||||
print(obj.foo)
|
||||
```
|
||||
|
||||
We support parsing:
|
||||
- `BaseModel`
|
||||
- `dict`
|
||||
- `list`
|
||||
- `Union`
|
||||
- `str`
|
||||
- `httpx.Response`
|
||||
"""
|
||||
cache_key = to if to is not None else self._cast_to
|
||||
cached = self._parsed_by_type.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached # type: ignore[no-any-return]
|
||||
|
||||
if not self._is_sse_stream:
|
||||
await self.read()
|
||||
|
||||
parsed = self._parse(to=to)
|
||||
if is_given(self._options.post_parser):
|
||||
parsed = self._options.post_parser(parsed)
|
||||
|
||||
self._parsed_by_type[cache_key] = parsed
|
||||
return parsed
|
||||
|
||||
async def read(self) -> bytes:
|
||||
"""Read and return the binary response content."""
|
||||
try:
|
||||
return await self.http_response.aread()
|
||||
except httpx.StreamConsumed as exc:
|
||||
# the default error raised by httpx isn't very
|
||||
# helpful in our case so we re-raise it with
|
||||
# a different error message
|
||||
raise StreamAlreadyConsumed() from exc
|
||||
|
||||
async def text(self) -> str:
|
||||
"""Read and decode the response content into a string."""
|
||||
await self.read()
|
||||
return self.http_response.text
|
||||
|
||||
async def json(self) -> object:
|
||||
"""Read and decode the JSON response content."""
|
||||
await self.read()
|
||||
return self.http_response.json()
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close the response and release the connection.
|
||||
|
||||
Automatically called if the response body is read to completion.
|
||||
"""
|
||||
await self.http_response.aclose()
|
||||
|
||||
async def iter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]:
|
||||
"""
|
||||
A byte-iterator over the decoded response content.
|
||||
|
||||
This automatically handles gzip, deflate and brotli encoded responses.
|
||||
"""
|
||||
async for chunk in self.http_response.aiter_bytes(chunk_size):
|
||||
yield chunk
|
||||
|
||||
async def iter_text(self, chunk_size: int | None = None) -> AsyncIterator[str]:
|
||||
"""A str-iterator over the decoded response content
|
||||
that handles both gzip, deflate, etc but also detects the content's
|
||||
string encoding.
|
||||
"""
|
||||
async for chunk in self.http_response.aiter_text(chunk_size):
|
||||
yield chunk
|
||||
|
||||
async def iter_lines(self) -> AsyncIterator[str]:
|
||||
"""Like `iter_text()` but will only yield chunks for each line"""
|
||||
async for chunk in self.http_response.aiter_lines():
|
||||
yield chunk
|
||||
|
||||
|
||||
class BinaryAPIResponse(APIResponse[bytes]):
|
||||
"""Subclass of APIResponse providing helpers for dealing with binary data.
|
||||
|
||||
Note: If you want to stream the response data instead of eagerly reading it
|
||||
all at once then you should use `.with_streaming_response` when making
|
||||
the API request, e.g. `.with_streaming_response.get_binary_response()`
|
||||
"""
|
||||
|
||||
def write_to_file(
|
||||
self,
|
||||
file: str | os.PathLike[str],
|
||||
) -> None:
|
||||
"""Write the output to the given file.
|
||||
|
||||
Accepts a filename or any path-like object, e.g. pathlib.Path
|
||||
|
||||
Note: if you want to stream the data to the file instead of writing
|
||||
all at once then you should use `.with_streaming_response` when making
|
||||
the API request, e.g. `.with_streaming_response.get_binary_response()`
|
||||
"""
|
||||
with open(file, mode="wb") as f:
|
||||
for data in self.iter_bytes():
|
||||
f.write(data)
|
||||
|
||||
|
||||
class AsyncBinaryAPIResponse(AsyncAPIResponse[bytes]):
|
||||
"""Subclass of APIResponse providing helpers for dealing with binary data.
|
||||
|
||||
Note: If you want to stream the response data instead of eagerly reading it
|
||||
all at once then you should use `.with_streaming_response` when making
|
||||
the API request, e.g. `.with_streaming_response.get_binary_response()`
|
||||
"""
|
||||
|
||||
async def write_to_file(
|
||||
self,
|
||||
file: str | os.PathLike[str],
|
||||
) -> None:
|
||||
"""Write the output to the given file.
|
||||
|
||||
Accepts a filename or any path-like object, e.g. pathlib.Path
|
||||
|
||||
Note: if you want to stream the data to the file instead of writing
|
||||
all at once then you should use `.with_streaming_response` when making
|
||||
the API request, e.g. `.with_streaming_response.get_binary_response()`
|
||||
"""
|
||||
path = anyio.Path(file)
|
||||
async with await path.open(mode="wb") as f:
|
||||
async for data in self.iter_bytes():
|
||||
await f.write(data)
|
||||
|
||||
|
||||
class StreamedBinaryAPIResponse(APIResponse[bytes]):
|
||||
def stream_to_file(
|
||||
self,
|
||||
file: str | os.PathLike[str],
|
||||
*,
|
||||
chunk_size: int | None = None,
|
||||
) -> None:
|
||||
"""Streams the output to the given file.
|
||||
|
||||
Accepts a filename or any path-like object, e.g. pathlib.Path
|
||||
"""
|
||||
with open(file, mode="wb") as f:
|
||||
for data in self.iter_bytes(chunk_size):
|
||||
f.write(data)
|
||||
|
||||
|
||||
class AsyncStreamedBinaryAPIResponse(AsyncAPIResponse[bytes]):
|
||||
async def stream_to_file(
|
||||
self,
|
||||
file: str | os.PathLike[str],
|
||||
*,
|
||||
chunk_size: int | None = None,
|
||||
) -> None:
|
||||
"""Streams the output to the given file.
|
||||
|
||||
Accepts a filename or any path-like object, e.g. pathlib.Path
|
||||
"""
|
||||
path = anyio.Path(file)
|
||||
async with await path.open(mode="wb") as f:
|
||||
async for data in self.iter_bytes(chunk_size):
|
||||
await f.write(data)
|
||||
|
||||
|
||||
class MissingStreamClassError(TypeError):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
"The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `google.genai._interactions._streaming` for reference",
|
||||
)
|
||||
|
||||
|
||||
class StreamAlreadyConsumed(GeminiNextGenAPIClientError):
|
||||
"""
|
||||
Attempted to read or stream content, but the content has already
|
||||
been streamed.
|
||||
|
||||
This can happen if you use a method like `.iter_lines()` and then attempt
|
||||
to read th entire response body afterwards, e.g.
|
||||
|
||||
```py
|
||||
response = await client.post(...)
|
||||
async for line in response.iter_lines():
|
||||
... # do something with `line`
|
||||
|
||||
content = await response.read()
|
||||
# ^ error
|
||||
```
|
||||
|
||||
If you want this behaviour you'll need to either manually accumulate the response
|
||||
content or call `await response.read()` before iterating over the stream.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
message = (
|
||||
"Attempted to read or stream some content, but the content has "
|
||||
"already been streamed. "
|
||||
"This could be due to attempting to stream the response "
|
||||
"content more than once."
|
||||
"\n\n"
|
||||
"You can fix this by manually accumulating the response content while streaming "
|
||||
"or by calling `.read()` before starting to stream."
|
||||
)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ResponseContextManager(Generic[_APIResponseT]):
|
||||
"""Context manager for ensuring that a request is not made
|
||||
until it is entered and that the response will always be closed
|
||||
when the context manager exits
|
||||
"""
|
||||
|
||||
def __init__(self, request_func: Callable[[], _APIResponseT]) -> None:
|
||||
self._request_func = request_func
|
||||
self.__response: _APIResponseT | None = None
|
||||
|
||||
def __enter__(self) -> _APIResponseT:
|
||||
self.__response = self._request_func()
|
||||
return self.__response
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
if self.__response is not None:
|
||||
self.__response.close()
|
||||
|
||||
|
||||
class AsyncResponseContextManager(Generic[_AsyncAPIResponseT]):
|
||||
"""Context manager for ensuring that a request is not made
|
||||
until it is entered and that the response will always be closed
|
||||
when the context manager exits
|
||||
"""
|
||||
|
||||
def __init__(self, api_request: Awaitable[_AsyncAPIResponseT]) -> None:
|
||||
self._api_request = api_request
|
||||
self.__response: _AsyncAPIResponseT | None = None
|
||||
|
||||
async def __aenter__(self) -> _AsyncAPIResponseT:
|
||||
self.__response = await self._api_request
|
||||
return self.__response
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
if self.__response is not None:
|
||||
await self.__response.close()
|
||||
|
||||
|
||||
def to_streamed_response_wrapper(func: Callable[P, R]) -> Callable[P, ResponseContextManager[APIResponse[R]]]:
|
||||
"""Higher order function that takes one of our bound API methods and wraps it
|
||||
to support streaming and returning the raw `APIResponse` object directly.
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[APIResponse[R]]:
|
||||
extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
|
||||
extra_headers[RAW_RESPONSE_HEADER] = "stream"
|
||||
|
||||
kwargs["extra_headers"] = extra_headers
|
||||
|
||||
make_request = functools.partial(func, *args, **kwargs)
|
||||
|
||||
return ResponseContextManager(cast(Callable[[], APIResponse[R]], make_request))
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def async_to_streamed_response_wrapper(
|
||||
func: Callable[P, Awaitable[R]],
|
||||
) -> Callable[P, AsyncResponseContextManager[AsyncAPIResponse[R]]]:
|
||||
"""Higher order function that takes one of our bound API methods and wraps it
|
||||
to support streaming and returning the raw `APIResponse` object directly.
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[AsyncAPIResponse[R]]:
|
||||
extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
|
||||
extra_headers[RAW_RESPONSE_HEADER] = "stream"
|
||||
|
||||
kwargs["extra_headers"] = extra_headers
|
||||
|
||||
make_request = func(*args, **kwargs)
|
||||
|
||||
return AsyncResponseContextManager(cast(Awaitable[AsyncAPIResponse[R]], make_request))
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def to_custom_streamed_response_wrapper(
|
||||
func: Callable[P, object],
|
||||
response_cls: type[_APIResponseT],
|
||||
) -> Callable[P, ResponseContextManager[_APIResponseT]]:
|
||||
"""Higher order function that takes one of our bound API methods and an `APIResponse` class
|
||||
and wraps the method to support streaming and returning the given response class directly.
|
||||
|
||||
Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])`
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[_APIResponseT]:
|
||||
extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
|
||||
extra_headers[RAW_RESPONSE_HEADER] = "stream"
|
||||
extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
|
||||
|
||||
kwargs["extra_headers"] = extra_headers
|
||||
|
||||
make_request = functools.partial(func, *args, **kwargs)
|
||||
|
||||
return ResponseContextManager(cast(Callable[[], _APIResponseT], make_request))
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def async_to_custom_streamed_response_wrapper(
|
||||
func: Callable[P, Awaitable[object]],
|
||||
response_cls: type[_AsyncAPIResponseT],
|
||||
) -> Callable[P, AsyncResponseContextManager[_AsyncAPIResponseT]]:
|
||||
"""Higher order function that takes one of our bound API methods and an `APIResponse` class
|
||||
and wraps the method to support streaming and returning the given response class directly.
|
||||
|
||||
Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])`
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[_AsyncAPIResponseT]:
|
||||
extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
|
||||
extra_headers[RAW_RESPONSE_HEADER] = "stream"
|
||||
extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
|
||||
|
||||
kwargs["extra_headers"] = extra_headers
|
||||
|
||||
make_request = func(*args, **kwargs)
|
||||
|
||||
return AsyncResponseContextManager(cast(Awaitable[_AsyncAPIResponseT], make_request))
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, APIResponse[R]]:
|
||||
"""Higher order function that takes one of our bound API methods and wraps it
|
||||
to support returning the raw `APIResponse` object directly.
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]:
|
||||
extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
|
||||
extra_headers[RAW_RESPONSE_HEADER] = "raw"
|
||||
|
||||
kwargs["extra_headers"] = extra_headers
|
||||
|
||||
return cast(APIResponse[R], func(*args, **kwargs))
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def async_to_raw_response_wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[AsyncAPIResponse[R]]]:
|
||||
"""Higher order function that takes one of our bound API methods and wraps it
|
||||
to support returning the raw `APIResponse` object directly.
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncAPIResponse[R]:
|
||||
extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
|
||||
extra_headers[RAW_RESPONSE_HEADER] = "raw"
|
||||
|
||||
kwargs["extra_headers"] = extra_headers
|
||||
|
||||
return cast(AsyncAPIResponse[R], await func(*args, **kwargs))
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def to_custom_raw_response_wrapper(
|
||||
func: Callable[P, object],
|
||||
response_cls: type[_APIResponseT],
|
||||
) -> Callable[P, _APIResponseT]:
|
||||
"""Higher order function that takes one of our bound API methods and an `APIResponse` class
|
||||
and wraps the method to support returning the given response class directly.
|
||||
|
||||
Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])`
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args: P.args, **kwargs: P.kwargs) -> _APIResponseT:
|
||||
extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
|
||||
extra_headers[RAW_RESPONSE_HEADER] = "raw"
|
||||
extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
|
||||
|
||||
kwargs["extra_headers"] = extra_headers
|
||||
|
||||
return cast(_APIResponseT, func(*args, **kwargs))
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def async_to_custom_raw_response_wrapper(
|
||||
func: Callable[P, Awaitable[object]],
|
||||
response_cls: type[_AsyncAPIResponseT],
|
||||
) -> Callable[P, Awaitable[_AsyncAPIResponseT]]:
|
||||
"""Higher order function that takes one of our bound API methods and an `APIResponse` class
|
||||
and wraps the method to support returning the given response class directly.
|
||||
|
||||
Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])`
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args: P.args, **kwargs: P.kwargs) -> Awaitable[_AsyncAPIResponseT]:
|
||||
extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
|
||||
extra_headers[RAW_RESPONSE_HEADER] = "raw"
|
||||
extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
|
||||
|
||||
kwargs["extra_headers"] = extra_headers
|
||||
|
||||
return cast(Awaitable[_AsyncAPIResponseT], func(*args, **kwargs))
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def extract_response_type(typ: type[BaseAPIResponse[Any]]) -> type:
|
||||
"""Given a type like `APIResponse[T]`, returns the generic type variable `T`.
|
||||
|
||||
This also handles the case where a concrete subclass is given, e.g.
|
||||
```py
|
||||
class MyResponse(APIResponse[bytes]):
|
||||
...
|
||||
|
||||
extract_response_type(MyResponse) -> bytes
|
||||
```
|
||||
"""
|
||||
return extract_type_var_from_base(
|
||||
typ,
|
||||
generic_bases=cast("tuple[type, ...]", (BaseAPIResponse, APIResponse, AsyncAPIResponse)),
|
||||
index=0,
|
||||
)
|
||||
@@ -0,0 +1,359 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import inspect
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast
|
||||
from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable
|
||||
|
||||
import httpx
|
||||
|
||||
from ._utils import extract_type_var_from_base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._client import GeminiNextGenAPIClient, AsyncGeminiNextGenAPIClient
|
||||
from ._models import FinalRequestOptions
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class Stream(Generic[_T]):
|
||||
"""Provides the core interface to iterate over a synchronous stream response."""
|
||||
|
||||
response: httpx.Response
|
||||
_options: Optional[FinalRequestOptions] = None
|
||||
_decoder: SSEBytesDecoder
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
cast_to: type[_T],
|
||||
response: httpx.Response,
|
||||
client: GeminiNextGenAPIClient,
|
||||
options: Optional[FinalRequestOptions] = None,
|
||||
) -> None:
|
||||
self.response = response
|
||||
self._cast_to = cast_to
|
||||
self._client = client
|
||||
self._options = options
|
||||
self._decoder = client._make_sse_decoder()
|
||||
self._iterator = self.__stream__()
|
||||
|
||||
def __next__(self) -> _T:
|
||||
return self._iterator.__next__()
|
||||
|
||||
def __iter__(self) -> Iterator[_T]:
|
||||
for item in self._iterator:
|
||||
yield item
|
||||
|
||||
def _iter_events(self) -> Iterator[ServerSentEvent]:
|
||||
yield from self._decoder.iter_bytes(self.response.iter_bytes())
|
||||
|
||||
def __stream__(self) -> Iterator[_T]:
|
||||
cast_to = cast(Any, self._cast_to)
|
||||
response = self.response
|
||||
process_data = self._client._process_response_data
|
||||
iterator = self._iter_events()
|
||||
|
||||
try:
|
||||
for sse in iterator:
|
||||
if sse.data.startswith("[DONE]"):
|
||||
break
|
||||
|
||||
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
|
||||
finally:
|
||||
# Ensure the response is closed even if the consumer doesn't read all data
|
||||
response.close()
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
self.close()
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close the response and release the connection.
|
||||
|
||||
Automatically called if the response body is read to completion.
|
||||
"""
|
||||
self.response.close()
|
||||
|
||||
|
||||
class AsyncStream(Generic[_T]):
|
||||
"""Provides the core interface to iterate over an asynchronous stream response."""
|
||||
|
||||
response: httpx.Response
|
||||
_options: Optional[FinalRequestOptions] = None
|
||||
_decoder: SSEDecoder | SSEBytesDecoder
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
cast_to: type[_T],
|
||||
response: httpx.Response,
|
||||
client: AsyncGeminiNextGenAPIClient,
|
||||
options: Optional[FinalRequestOptions] = None,
|
||||
) -> None:
|
||||
self.response = response
|
||||
self._cast_to = cast_to
|
||||
self._client = client
|
||||
self._options = options
|
||||
self._decoder = client._make_sse_decoder()
|
||||
self._iterator = self.__stream__()
|
||||
|
||||
async def __anext__(self) -> _T:
|
||||
return await self._iterator.__anext__()
|
||||
|
||||
async def __aiter__(self) -> AsyncIterator[_T]:
|
||||
async for item in self._iterator:
|
||||
yield item
|
||||
|
||||
async def _iter_events(self) -> AsyncIterator[ServerSentEvent]:
|
||||
async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()):
|
||||
yield sse
|
||||
|
||||
async def __stream__(self) -> AsyncIterator[_T]:
|
||||
cast_to = cast(Any, self._cast_to)
|
||||
response = self.response
|
||||
process_data = self._client._process_response_data
|
||||
iterator = self._iter_events()
|
||||
|
||||
try:
|
||||
async for sse in iterator:
|
||||
if sse.data.startswith("[DONE]"):
|
||||
break
|
||||
|
||||
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
|
||||
finally:
|
||||
# Ensure the response is closed even if the consumer doesn't read all data
|
||||
await response.aclose()
|
||||
|
||||
async def __aenter__(self) -> Self:
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
await self.close()
|
||||
|
||||
async def close(self) -> None:
|
||||
"""
|
||||
Close the response and release the connection.
|
||||
|
||||
Automatically called if the response body is read to completion.
|
||||
"""
|
||||
await self.response.aclose()
|
||||
|
||||
|
||||
class ServerSentEvent:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
event: str | None = None,
|
||||
data: str | None = None,
|
||||
id: str | None = None,
|
||||
retry: int | None = None,
|
||||
) -> None:
|
||||
if data is None:
|
||||
data = ""
|
||||
|
||||
self._id = id
|
||||
self._data = data
|
||||
self._event = event or None
|
||||
self._retry = retry
|
||||
|
||||
@property
|
||||
def event(self) -> str | None:
|
||||
return self._event
|
||||
|
||||
@property
|
||||
def id(self) -> str | None:
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def retry(self) -> int | None:
|
||||
return self._retry
|
||||
|
||||
@property
|
||||
def data(self) -> str:
|
||||
return self._data
|
||||
|
||||
def json(self) -> Any:
|
||||
return json.loads(self.data)
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})"
|
||||
|
||||
|
||||
class SSEDecoder:
|
||||
_data: list[str]
|
||||
_event: str | None
|
||||
_retry: int | None
|
||||
_last_event_id: str | None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._event = None
|
||||
self._data = []
|
||||
self._last_event_id = None
|
||||
self._retry = None
|
||||
|
||||
def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]:
|
||||
"""Given an iterator that yields raw binary data, iterate over it & yield every event encountered"""
|
||||
for chunk in self._iter_chunks(iterator):
|
||||
# Split before decoding so splitlines() only uses \r and \n
|
||||
for raw_line in chunk.splitlines():
|
||||
line = raw_line.decode("utf-8")
|
||||
sse = self.decode(line)
|
||||
if sse:
|
||||
yield sse
|
||||
|
||||
def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]:
|
||||
"""Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks"""
|
||||
data = b""
|
||||
for chunk in iterator:
|
||||
for line in chunk.splitlines(keepends=True):
|
||||
data += line
|
||||
if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")):
|
||||
yield data
|
||||
data = b""
|
||||
if data:
|
||||
yield data
|
||||
|
||||
async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]:
|
||||
"""Given an iterator that yields raw binary data, iterate over it & yield every event encountered"""
|
||||
async for chunk in self._aiter_chunks(iterator):
|
||||
# Split before decoding so splitlines() only uses \r and \n
|
||||
for raw_line in chunk.splitlines():
|
||||
line = raw_line.decode("utf-8")
|
||||
sse = self.decode(line)
|
||||
if sse:
|
||||
yield sse
|
||||
|
||||
async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
|
||||
"""Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks"""
|
||||
data = b""
|
||||
async for chunk in iterator:
|
||||
for line in chunk.splitlines(keepends=True):
|
||||
data += line
|
||||
if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")):
|
||||
yield data
|
||||
data = b""
|
||||
if data:
|
||||
yield data
|
||||
|
||||
def decode(self, line: str) -> ServerSentEvent | None:
|
||||
# See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501
|
||||
|
||||
if not line:
|
||||
if not self._event and not self._data and not self._last_event_id and self._retry is None:
|
||||
return None
|
||||
|
||||
sse = ServerSentEvent(
|
||||
event=self._event,
|
||||
data="\n".join(self._data),
|
||||
id=self._last_event_id,
|
||||
retry=self._retry,
|
||||
)
|
||||
|
||||
# NOTE: as per the SSE spec, do not reset last_event_id.
|
||||
self._event = None
|
||||
self._data = []
|
||||
self._retry = None
|
||||
|
||||
return sse
|
||||
|
||||
if line.startswith(":"):
|
||||
return None
|
||||
|
||||
fieldname, _, value = line.partition(":")
|
||||
|
||||
if value.startswith(" "):
|
||||
value = value[1:]
|
||||
|
||||
if fieldname == "event":
|
||||
self._event = value
|
||||
elif fieldname == "data":
|
||||
self._data.append(value)
|
||||
elif fieldname == "id":
|
||||
if "\0" in value:
|
||||
pass
|
||||
else:
|
||||
self._last_event_id = value
|
||||
elif fieldname == "retry":
|
||||
try:
|
||||
self._retry = int(value)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
else:
|
||||
pass # Field is ignored.
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SSEBytesDecoder(Protocol):
|
||||
def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]:
|
||||
"""Given an iterator that yields raw binary data, iterate over it & yield every event encountered"""
|
||||
...
|
||||
|
||||
def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]:
|
||||
"""Given an async iterator that yields raw binary data, iterate over it & yield every event encountered"""
|
||||
...
|
||||
|
||||
|
||||
def is_stream_class_type(typ: type) -> TypeGuard[type[Stream[object]] | type[AsyncStream[object]]]:
|
||||
"""TypeGuard for determining whether or not the given type is a subclass of `Stream` / `AsyncStream`"""
|
||||
origin = get_origin(typ) or typ
|
||||
return inspect.isclass(origin) and issubclass(origin, (Stream, AsyncStream))
|
||||
|
||||
|
||||
def extract_stream_chunk_type(
|
||||
stream_cls: type,
|
||||
*,
|
||||
failure_message: str | None = None,
|
||||
) -> type:
|
||||
"""Given a type like `Stream[T]`, returns the generic type variable `T`.
|
||||
|
||||
This also handles the case where a concrete subclass is given, e.g.
|
||||
```py
|
||||
class MyStream(Stream[bytes]):
|
||||
...
|
||||
|
||||
extract_stream_chunk_type(MyStream) -> bytes
|
||||
```
|
||||
"""
|
||||
from ._base_client import Stream, AsyncStream
|
||||
|
||||
return extract_type_var_from_base(
|
||||
stream_cls,
|
||||
index=0,
|
||||
generic_bases=cast("tuple[type, ...]", (Stream, AsyncStream)),
|
||||
failure_message=failure_message,
|
||||
)
|
||||
@@ -0,0 +1,285 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from os import PathLike
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Type,
|
||||
Tuple,
|
||||
Union,
|
||||
Mapping,
|
||||
TypeVar,
|
||||
Callable,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Optional,
|
||||
Sequence,
|
||||
AsyncIterable,
|
||||
)
|
||||
from typing_extensions import (
|
||||
Set,
|
||||
Literal,
|
||||
Protocol,
|
||||
TypeAlias,
|
||||
TypedDict,
|
||||
SupportsIndex,
|
||||
overload,
|
||||
override,
|
||||
runtime_checkable,
|
||||
)
|
||||
|
||||
import httpx
|
||||
import pydantic
|
||||
from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._models import BaseModel
|
||||
from ._response import APIResponse, AsyncAPIResponse
|
||||
|
||||
Transport = BaseTransport
|
||||
AsyncTransport = AsyncBaseTransport
|
||||
Query = Mapping[str, object]
|
||||
Body = object
|
||||
AnyMapping = Mapping[str, object]
|
||||
ModelT = TypeVar("ModelT", bound=pydantic.BaseModel)
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
# Approximates httpx internal ProxiesTypes and RequestFiles types
|
||||
# while adding support for `PathLike` instances
|
||||
ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]]
|
||||
ProxiesTypes = Union[str, Proxy, ProxiesDict]
|
||||
if TYPE_CHECKING:
|
||||
Base64FileInput = Union[IO[bytes], PathLike[str]]
|
||||
FileContent = Union[IO[bytes], bytes, PathLike[str]]
|
||||
else:
|
||||
Base64FileInput = Union[IO[bytes], PathLike]
|
||||
FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8.
|
||||
|
||||
|
||||
# Used for sending raw binary data / streaming data in request bodies
|
||||
# e.g. for file uploads without multipart encoding
|
||||
BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]]
|
||||
AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]]
|
||||
|
||||
FileTypes = Union[
|
||||
# file (or bytes)
|
||||
FileContent,
|
||||
# (filename, file (or bytes))
|
||||
Tuple[Optional[str], FileContent],
|
||||
# (filename, file (or bytes), content_type)
|
||||
Tuple[Optional[str], FileContent, Optional[str]],
|
||||
# (filename, file (or bytes), content_type, headers)
|
||||
Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]],
|
||||
]
|
||||
RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]]
|
||||
|
||||
# duplicate of the above but without our custom file support
|
||||
HttpxFileContent = Union[IO[bytes], bytes]
|
||||
HttpxFileTypes = Union[
|
||||
# file (or bytes)
|
||||
HttpxFileContent,
|
||||
# (filename, file (or bytes))
|
||||
Tuple[Optional[str], HttpxFileContent],
|
||||
# (filename, file (or bytes), content_type)
|
||||
Tuple[Optional[str], HttpxFileContent, Optional[str]],
|
||||
# (filename, file (or bytes), content_type, headers)
|
||||
Tuple[Optional[str], HttpxFileContent, Optional[str], Mapping[str, str]],
|
||||
]
|
||||
HttpxRequestFiles = Union[Mapping[str, HttpxFileTypes], Sequence[Tuple[str, HttpxFileTypes]]]
|
||||
|
||||
# Workaround to support (cast_to: Type[ResponseT]) -> ResponseT
|
||||
# where ResponseT includes `None`. In order to support directly
|
||||
# passing `None`, overloads would have to be defined for every
|
||||
# method that uses `ResponseT` which would lead to an unacceptable
|
||||
# amount of code duplication and make it unreadable. See _base_client.py
|
||||
# for example usage.
|
||||
#
|
||||
# This unfortunately means that you will either have
|
||||
# to import this type and pass it explicitly:
|
||||
#
|
||||
# from google.genai._interactions import NoneType
|
||||
# client.get('/foo', cast_to=NoneType)
|
||||
#
|
||||
# or build it yourself:
|
||||
#
|
||||
# client.get('/foo', cast_to=type(None))
|
||||
if TYPE_CHECKING:
|
||||
NoneType: Type[None]
|
||||
else:
|
||||
NoneType = type(None)
|
||||
|
||||
|
||||
class RequestOptions(TypedDict, total=False):
|
||||
headers: Headers
|
||||
max_retries: int
|
||||
timeout: float | Timeout | None
|
||||
params: Query
|
||||
extra_json: AnyMapping
|
||||
idempotency_key: str
|
||||
follow_redirects: bool
|
||||
|
||||
|
||||
# Sentinel class used until PEP 0661 is accepted
|
||||
class NotGiven:
|
||||
"""
|
||||
For parameters with a meaningful None value, we need to distinguish between
|
||||
the user explicitly passing None, and the user not passing the parameter at
|
||||
all.
|
||||
|
||||
User code shouldn't need to use not_given directly.
|
||||
|
||||
For example:
|
||||
|
||||
```py
|
||||
def create(timeout: Timeout | None | NotGiven = not_given): ...
|
||||
|
||||
|
||||
create(timeout=1) # 1s timeout
|
||||
create(timeout=None) # No timeout
|
||||
create() # Default timeout behavior
|
||||
```
|
||||
"""
|
||||
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return "NOT_GIVEN"
|
||||
|
||||
|
||||
not_given = NotGiven()
|
||||
# for backwards compatibility:
|
||||
NOT_GIVEN = NotGiven()
|
||||
|
||||
|
||||
class Omit:
|
||||
"""
|
||||
To explicitly omit something from being sent in a request, use `omit`.
|
||||
|
||||
```py
|
||||
# as the default `Content-Type` header is `application/json` that will be sent
|
||||
client.post("/upload/files", files={"file": b"my raw file content"})
|
||||
|
||||
# you can't explicitly override the header as it has to be dynamically generated
|
||||
# to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983'
|
||||
client.post(..., headers={"Content-Type": "multipart/form-data"})
|
||||
|
||||
# instead you can remove the default `application/json` header by passing omit
|
||||
client.post(..., headers={"Content-Type": omit})
|
||||
```
|
||||
"""
|
||||
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
|
||||
omit = Omit()
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ModelBuilderProtocol(Protocol):
|
||||
@classmethod
|
||||
def build(
|
||||
cls: type[_T],
|
||||
*,
|
||||
response: Response,
|
||||
data: object,
|
||||
) -> _T: ...
|
||||
|
||||
|
||||
Headers = Mapping[str, Union[str, Omit]]
|
||||
|
||||
|
||||
class HeadersLikeProtocol(Protocol):
|
||||
def get(self, __key: str) -> str | None: ...
|
||||
|
||||
|
||||
HeadersLike = Union[Headers, HeadersLikeProtocol]
|
||||
|
||||
ResponseT = TypeVar(
|
||||
"ResponseT",
|
||||
bound=Union[
|
||||
object,
|
||||
str,
|
||||
None,
|
||||
"BaseModel",
|
||||
List[Any],
|
||||
Dict[str, Any],
|
||||
Response,
|
||||
ModelBuilderProtocol,
|
||||
"APIResponse[Any]",
|
||||
"AsyncAPIResponse[Any]",
|
||||
],
|
||||
)
|
||||
|
||||
StrBytesIntFloat = Union[str, bytes, int, float]
|
||||
|
||||
# Note: copied from Pydantic
|
||||
# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79
|
||||
IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]]
|
||||
|
||||
PostParser = Callable[[Any], Any]
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class InheritsGeneric(Protocol):
|
||||
"""Represents a type that has inherited from `Generic`
|
||||
|
||||
The `__orig_bases__` property can be used to determine the resolved
|
||||
type variable for a given base class.
|
||||
"""
|
||||
|
||||
__orig_bases__: tuple[_GenericAlias]
|
||||
|
||||
|
||||
class _GenericAlias(Protocol):
|
||||
__origin__: type[object]
|
||||
|
||||
|
||||
class HttpxSendArgs(TypedDict, total=False):
|
||||
auth: httpx.Auth
|
||||
follow_redirects: bool
|
||||
|
||||
|
||||
_T_co = TypeVar("_T_co", covariant=True)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This works because str.__contains__ does not accept object (either in typeshed or at runtime)
|
||||
# https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285
|
||||
#
|
||||
# Note: index() and count() methods are intentionally omitted to allow pyright to properly
|
||||
# infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr.
|
||||
class SequenceNotStr(Protocol[_T_co]):
|
||||
@overload
|
||||
def __getitem__(self, index: SupportsIndex, /) -> _T_co: ...
|
||||
@overload
|
||||
def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ...
|
||||
def __contains__(self, value: object, /) -> bool: ...
|
||||
def __len__(self) -> int: ...
|
||||
def __iter__(self) -> Iterator[_T_co]: ...
|
||||
def __reversed__(self) -> Iterator[_T_co]: ...
|
||||
else:
|
||||
# just point this to a normal `Sequence` at runtime to avoid having to special case
|
||||
# deserializing our custom sequence type
|
||||
SequenceNotStr = Sequence
|
||||
@@ -0,0 +1,80 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from ._path import path_template as path_template
|
||||
from ._sync import asyncify as asyncify
|
||||
from ._proxy import LazyProxy as LazyProxy
|
||||
from ._utils import (
|
||||
flatten as flatten,
|
||||
is_dict as is_dict,
|
||||
is_list as is_list,
|
||||
is_given as is_given,
|
||||
is_tuple as is_tuple,
|
||||
json_safe as json_safe,
|
||||
lru_cache as lru_cache,
|
||||
is_mapping as is_mapping,
|
||||
is_tuple_t as is_tuple_t,
|
||||
is_iterable as is_iterable,
|
||||
is_sequence as is_sequence,
|
||||
coerce_float as coerce_float,
|
||||
is_mapping_t as is_mapping_t,
|
||||
removeprefix as removeprefix,
|
||||
removesuffix as removesuffix,
|
||||
extract_files as extract_files,
|
||||
is_sequence_t as is_sequence_t,
|
||||
required_args as required_args,
|
||||
coerce_boolean as coerce_boolean,
|
||||
coerce_integer as coerce_integer,
|
||||
file_from_path as file_from_path,
|
||||
strip_not_given as strip_not_given,
|
||||
deepcopy_minimal as deepcopy_minimal,
|
||||
get_async_library as get_async_library,
|
||||
maybe_coerce_float as maybe_coerce_float,
|
||||
get_required_header as get_required_header,
|
||||
maybe_coerce_boolean as maybe_coerce_boolean,
|
||||
maybe_coerce_integer as maybe_coerce_integer,
|
||||
)
|
||||
from ._compat import (
|
||||
get_args as get_args,
|
||||
is_union as is_union,
|
||||
get_origin as get_origin,
|
||||
is_typeddict as is_typeddict,
|
||||
is_literal_type as is_literal_type,
|
||||
)
|
||||
from ._typing import (
|
||||
is_list_type as is_list_type,
|
||||
is_union_type as is_union_type,
|
||||
extract_type_arg as extract_type_arg,
|
||||
is_iterable_type as is_iterable_type,
|
||||
is_required_type as is_required_type,
|
||||
is_sequence_type as is_sequence_type,
|
||||
is_annotated_type as is_annotated_type,
|
||||
is_type_alias_type as is_type_alias_type,
|
||||
strip_annotated_type as strip_annotated_type,
|
||||
extract_type_var_from_base as extract_type_var_from_base,
|
||||
)
|
||||
from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator
|
||||
from ._transform import (
|
||||
PropertyInfo as PropertyInfo,
|
||||
transform as transform,
|
||||
async_transform as async_transform,
|
||||
maybe_transform as maybe_transform,
|
||||
async_maybe_transform as async_maybe_transform,
|
||||
)
|
||||
from ._reflection import (
|
||||
function_has_argument as function_has_argument,
|
||||
assert_signatures_in_sync as assert_signatures_in_sync,
|
||||
)
|
||||
from ._datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime
|
||||
@@ -0,0 +1,61 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import typing_extensions
|
||||
from typing import Any, Type, Union, Literal, Optional
|
||||
from datetime import date, datetime
|
||||
from typing_extensions import get_args as _get_args, get_origin as _get_origin
|
||||
|
||||
from .._types import StrBytesIntFloat
|
||||
from ._datetime_parse import parse_date as _parse_date, parse_datetime as _parse_datetime
|
||||
|
||||
_LITERAL_TYPES = {Literal, typing_extensions.Literal}
|
||||
|
||||
|
||||
def get_args(tp: type[Any]) -> tuple[Any, ...]:
|
||||
return _get_args(tp)
|
||||
|
||||
|
||||
def get_origin(tp: type[Any]) -> type[Any] | None:
|
||||
return _get_origin(tp)
|
||||
|
||||
|
||||
def is_union(tp: Optional[Type[Any]]) -> bool:
|
||||
if sys.version_info < (3, 10):
|
||||
return tp is Union # type: ignore[comparison-overlap]
|
||||
else:
|
||||
import types
|
||||
|
||||
return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap]
|
||||
|
||||
|
||||
def is_typeddict(tp: Type[Any]) -> bool:
|
||||
return typing_extensions.is_typeddict(tp)
|
||||
|
||||
|
||||
def is_literal_type(tp: Type[Any]) -> bool:
|
||||
return get_origin(tp) in _LITERAL_TYPES
|
||||
|
||||
|
||||
def parse_date(value: Union[date, StrBytesIntFloat]) -> date:
|
||||
return _parse_date(value)
|
||||
|
||||
|
||||
def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime:
|
||||
return _parse_datetime(value)
|
||||
@@ -0,0 +1,151 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py
|
||||
without the Pydantic v1 specific errors.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Dict, Union, Optional
|
||||
from datetime import date, datetime, timezone, timedelta
|
||||
|
||||
from .._types import StrBytesIntFloat
|
||||
|
||||
date_expr = r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
|
||||
time_expr = (
|
||||
r"(?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
|
||||
r"(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?"
|
||||
r"(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
|
||||
)
|
||||
|
||||
date_re = re.compile(f"{date_expr}$")
|
||||
datetime_re = re.compile(f"{date_expr}[T ]{time_expr}")
|
||||
|
||||
|
||||
EPOCH = datetime(1970, 1, 1)
|
||||
# if greater than this, the number is in ms, if less than or equal it's in seconds
|
||||
# (in seconds this is 11th October 2603, in ms it's 20th August 1970)
|
||||
MS_WATERSHED = int(2e10)
|
||||
# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9
|
||||
MAX_NUMBER = int(3e20)
|
||||
|
||||
|
||||
def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]:
|
||||
if isinstance(value, (int, float)):
|
||||
return value
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return None
|
||||
except TypeError:
|
||||
raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None
|
||||
|
||||
|
||||
def _from_unix_seconds(seconds: Union[int, float]) -> datetime:
|
||||
if seconds > MAX_NUMBER:
|
||||
return datetime.max
|
||||
elif seconds < -MAX_NUMBER:
|
||||
return datetime.min
|
||||
|
||||
while abs(seconds) > MS_WATERSHED:
|
||||
seconds /= 1000
|
||||
dt = EPOCH + timedelta(seconds=seconds)
|
||||
return dt.replace(tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]:
|
||||
if value == "Z":
|
||||
return timezone.utc
|
||||
elif value is not None:
|
||||
offset_mins = int(value[-2:]) if len(value) > 3 else 0
|
||||
offset = 60 * int(value[1:3]) + offset_mins
|
||||
if value[0] == "-":
|
||||
offset = -offset
|
||||
return timezone(timedelta(minutes=offset))
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime:
|
||||
"""
|
||||
Parse a datetime/int/float/string and return a datetime.datetime.
|
||||
|
||||
This function supports time zone offsets. When the input contains one,
|
||||
the output uses a timezone with a fixed offset from UTC.
|
||||
|
||||
Raise ValueError if the input is well formatted but not a valid datetime.
|
||||
Raise ValueError if the input isn't well formatted.
|
||||
"""
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
|
||||
number = _get_numeric(value, "datetime")
|
||||
if number is not None:
|
||||
return _from_unix_seconds(number)
|
||||
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode()
|
||||
|
||||
assert not isinstance(value, (float, int))
|
||||
|
||||
match = datetime_re.match(value)
|
||||
if match is None:
|
||||
raise ValueError("invalid datetime format")
|
||||
|
||||
kw = match.groupdict()
|
||||
if kw["microsecond"]:
|
||||
kw["microsecond"] = kw["microsecond"].ljust(6, "0")
|
||||
|
||||
tzinfo = _parse_timezone(kw.pop("tzinfo"))
|
||||
kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None}
|
||||
kw_["tzinfo"] = tzinfo
|
||||
|
||||
return datetime(**kw_) # type: ignore
|
||||
|
||||
|
||||
def parse_date(value: Union[date, StrBytesIntFloat]) -> date:
|
||||
"""
|
||||
Parse a date/int/float/string and return a datetime.date.
|
||||
|
||||
Raise ValueError if the input is well formatted but not a valid date.
|
||||
Raise ValueError if the input isn't well formatted.
|
||||
"""
|
||||
if isinstance(value, date):
|
||||
if isinstance(value, datetime):
|
||||
return value.date()
|
||||
else:
|
||||
return value
|
||||
|
||||
number = _get_numeric(value, "date")
|
||||
if number is not None:
|
||||
return _from_unix_seconds(number).date()
|
||||
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode()
|
||||
|
||||
assert not isinstance(value, (float, int))
|
||||
match = date_re.match(value)
|
||||
if match is None:
|
||||
raise ValueError("invalid date format")
|
||||
|
||||
kw = {k: int(v) for k, v in match.groupdict().items()}
|
||||
|
||||
try:
|
||||
return date(**kw)
|
||||
except ValueError:
|
||||
raise ValueError("invalid date format") from None
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
from datetime import datetime
|
||||
from typing_extensions import override
|
||||
|
||||
import pydantic
|
||||
|
||||
from .._compat import model_dump
|
||||
|
||||
|
||||
def openapi_dumps(obj: Any) -> bytes:
|
||||
"""
|
||||
Serialize an object to UTF-8 encoded JSON bytes.
|
||||
|
||||
Extends the standard json.dumps with support for additional types
|
||||
commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc.
|
||||
"""
|
||||
return json.dumps(
|
||||
obj,
|
||||
cls=_CustomEncoder,
|
||||
# Uses the same defaults as httpx's JSON serialization
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
class _CustomEncoder(json.JSONEncoder):
|
||||
@override
|
||||
def default(self, o: Any) -> Any:
|
||||
if isinstance(o, datetime):
|
||||
return o.isoformat()
|
||||
if isinstance(o, pydantic.BaseModel):
|
||||
return model_dump(o, exclude_unset=True, mode="json", by_alias=True)
|
||||
return super().default(o)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger: logging.Logger = logging.getLogger("google.genai._interactions")
|
||||
httpx_logger: logging.Logger = logging.getLogger("httpx")
|
||||
|
||||
|
||||
def _basic_config() -> None:
|
||||
# e.g. [2023-10-05 14:12:26 - google.genai._interactions._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK"
|
||||
logging.basicConfig(
|
||||
format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
env = os.environ.get("GEMINI_NEXT_GEN_API_LOG")
|
||||
if env == "debug":
|
||||
_basic_config()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
httpx_logger.setLevel(logging.DEBUG)
|
||||
elif env == "info":
|
||||
_basic_config()
|
||||
logger.setLevel(logging.INFO)
|
||||
httpx_logger.setLevel(logging.INFO)
|
||||
@@ -0,0 +1,142 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import (
|
||||
Any,
|
||||
Mapping,
|
||||
Callable,
|
||||
)
|
||||
from urllib.parse import quote
|
||||
|
||||
# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E).
|
||||
_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$")
|
||||
|
||||
_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}")
|
||||
|
||||
|
||||
def _quote_path_segment_part(value: str) -> str:
|
||||
"""Percent-encode `value` for use in a URI path segment.
|
||||
|
||||
Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe.
|
||||
https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
|
||||
"""
|
||||
# quote() already treats unreserved characters (letters, digits, and -._~)
|
||||
# as safe, so we only need to add sub-delims, ':', and '@'.
|
||||
# Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted.
|
||||
return quote(value, safe="!$&'()*+,;=:@")
|
||||
|
||||
|
||||
def _quote_query_part(value: str) -> str:
|
||||
"""Percent-encode `value` for use in a URI query string.
|
||||
|
||||
Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe.
|
||||
https://datatracker.ietf.org/doc/html/rfc3986#section-3.4
|
||||
"""
|
||||
return quote(value, safe="!$'()*+,;:@/?")
|
||||
|
||||
|
||||
def _quote_fragment_part(value: str) -> str:
|
||||
"""Percent-encode `value` for use in a URI fragment.
|
||||
|
||||
Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe.
|
||||
https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
|
||||
"""
|
||||
return quote(value, safe="!$&'()*+,;=:@/?")
|
||||
|
||||
|
||||
def _interpolate(
|
||||
template: str,
|
||||
values: Mapping[str, Any],
|
||||
quoter: Callable[[str], str],
|
||||
) -> str:
|
||||
"""Replace {name} placeholders in `template`, quoting each value with `quoter`.
|
||||
|
||||
Placeholder names are looked up in `values`.
|
||||
|
||||
Raises:
|
||||
KeyError: If a placeholder is not found in `values`.
|
||||
"""
|
||||
# re.split with a capturing group returns alternating
|
||||
# [text, name, text, name, ..., text] elements.
|
||||
parts = _PLACEHOLDER_RE.split(template)
|
||||
|
||||
for i in range(1, len(parts), 2):
|
||||
name = parts[i]
|
||||
if name not in values:
|
||||
raise KeyError(f"a value for placeholder {{{name}}} was not provided")
|
||||
val = values[name]
|
||||
if val is None:
|
||||
parts[i] = "null"
|
||||
elif isinstance(val, bool):
|
||||
parts[i] = "true" if val else "false"
|
||||
else:
|
||||
parts[i] = quoter(str(values[name]))
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def path_template(template: str, /, **kwargs: Any) -> str:
|
||||
"""Interpolate {name} placeholders in `template` from keyword arguments.
|
||||
|
||||
Args:
|
||||
template: The template string containing {name} placeholders.
|
||||
**kwargs: Keyword arguments to interpolate into the template.
|
||||
|
||||
Returns:
|
||||
The template with placeholders interpolated and percent-encoded.
|
||||
|
||||
Safe characters for percent-encoding are dependent on the URI component.
|
||||
Placeholders in path and fragment portions are percent-encoded where the `segment`
|
||||
and `fragment` sets from RFC 3986 respectively are considered safe.
|
||||
Placeholders in the query portion are percent-encoded where the `query` set from
|
||||
RFC 3986 §3.3 is considered safe except for = and & characters.
|
||||
|
||||
Raises:
|
||||
KeyError: If a placeholder is not found in `kwargs`.
|
||||
ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments).
|
||||
"""
|
||||
# Split the template into path, query, and fragment portions.
|
||||
fragment_template: str | None = None
|
||||
query_template: str | None = None
|
||||
|
||||
rest = template
|
||||
if "#" in rest:
|
||||
rest, fragment_template = rest.split("#", 1)
|
||||
if "?" in rest:
|
||||
rest, query_template = rest.split("?", 1)
|
||||
path_template = rest
|
||||
|
||||
# Interpolate each portion with the appropriate quoting rules.
|
||||
path_result = _interpolate(path_template, kwargs, _quote_path_segment_part)
|
||||
|
||||
# Reject dot-segments (. and ..) in the final assembled path. The check
|
||||
# runs after interpolation so that adjacent placeholders or a mix of static
|
||||
# text and placeholders that together form a dot-segment are caught.
|
||||
# Also reject percent-encoded dot-segments to protect against incorrectly
|
||||
# implemented normalization in servers/proxies.
|
||||
for segment in path_result.split("/"):
|
||||
if _DOT_SEGMENT_RE.match(segment):
|
||||
raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed")
|
||||
|
||||
result = path_result
|
||||
if query_template is not None:
|
||||
result += "?" + _interpolate(query_template, kwargs, _quote_query_part)
|
||||
if fragment_template is not None:
|
||||
result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,80 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Generic, TypeVar, Iterable, cast
|
||||
from typing_extensions import override
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class LazyProxy(Generic[T], ABC):
|
||||
"""Implements data methods to pretend that an instance is another instance.
|
||||
|
||||
This includes forwarding attribute access and other methods.
|
||||
"""
|
||||
|
||||
# Note: we have to special case proxies that themselves return proxies
|
||||
# to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz`
|
||||
|
||||
def __getattr__(self, attr: str) -> object:
|
||||
proxied = self.__get_proxied__()
|
||||
if isinstance(proxied, LazyProxy):
|
||||
return proxied # pyright: ignore
|
||||
return getattr(proxied, attr)
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
proxied = self.__get_proxied__()
|
||||
if isinstance(proxied, LazyProxy):
|
||||
return proxied.__class__.__name__
|
||||
return repr(self.__get_proxied__())
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
proxied = self.__get_proxied__()
|
||||
if isinstance(proxied, LazyProxy):
|
||||
return proxied.__class__.__name__
|
||||
return str(proxied)
|
||||
|
||||
@override
|
||||
def __dir__(self) -> Iterable[str]:
|
||||
proxied = self.__get_proxied__()
|
||||
if isinstance(proxied, LazyProxy):
|
||||
return []
|
||||
return proxied.__dir__()
|
||||
|
||||
@property # type: ignore
|
||||
@override
|
||||
def __class__(self) -> type: # pyright: ignore
|
||||
try:
|
||||
proxied = self.__get_proxied__()
|
||||
except Exception:
|
||||
return type(self)
|
||||
if issubclass(type(proxied), LazyProxy):
|
||||
return type(proxied)
|
||||
return proxied.__class__
|
||||
|
||||
def __get_proxied__(self) -> T:
|
||||
return self.__load__()
|
||||
|
||||
def __as_proxied__(self) -> T:
|
||||
"""Helper method that returns the current proxy, typed as the loaded object"""
|
||||
return cast(T, self)
|
||||
|
||||
@abstractmethod
|
||||
def __load__(self) -> T: ...
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool:
|
||||
"""Returns whether or not the given function has a specific parameter"""
|
||||
sig = inspect.signature(func)
|
||||
return arg_name in sig.parameters
|
||||
|
||||
|
||||
def assert_signatures_in_sync(
|
||||
source_func: Callable[..., Any],
|
||||
check_func: Callable[..., Any],
|
||||
*,
|
||||
exclude_params: set[str] = set(),
|
||||
) -> None:
|
||||
"""Ensure that the signature of the second function matches the first."""
|
||||
|
||||
check_sig = inspect.signature(check_func)
|
||||
source_sig = inspect.signature(source_func)
|
||||
|
||||
errors: list[str] = []
|
||||
|
||||
for name, source_param in source_sig.parameters.items():
|
||||
if name in exclude_params:
|
||||
continue
|
||||
|
||||
custom_param = check_sig.parameters.get(name)
|
||||
if not custom_param:
|
||||
errors.append(f"the `{name}` param is missing")
|
||||
continue
|
||||
|
||||
if custom_param.annotation != source_param.annotation:
|
||||
errors.append(
|
||||
f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}"
|
||||
)
|
||||
continue
|
||||
|
||||
if errors:
|
||||
raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors))
|
||||
@@ -0,0 +1,39 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing_extensions import override
|
||||
|
||||
from ._proxy import LazyProxy
|
||||
|
||||
|
||||
class ResourcesProxy(LazyProxy[Any]):
|
||||
"""A proxy for the `google.genai._interactions.resources` module.
|
||||
|
||||
This is used so that we can lazily import `google.genai._interactions.resources` only when
|
||||
needed *and* so that users can just import `google.genai._interactions` and reference `google.genai._interactions.resources`
|
||||
"""
|
||||
|
||||
@override
|
||||
def __load__(self) -> Any:
|
||||
import importlib
|
||||
|
||||
mod = importlib.import_module("google.genai._interactions.resources")
|
||||
return mod
|
||||
|
||||
|
||||
resources = ResourcesProxy().__as_proxied__()
|
||||
@@ -0,0 +1,27 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from typing import Any
|
||||
from typing_extensions import Iterator, AsyncIterator
|
||||
|
||||
|
||||
def consume_sync_iterator(iterator: Iterator[Any]) -> None:
|
||||
for _ in iterator:
|
||||
...
|
||||
|
||||
|
||||
async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None:
|
||||
async for _ in iterator:
|
||||
...
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
from typing import TypeVar, Callable, Awaitable
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
import anyio
|
||||
import sniffio
|
||||
import anyio.to_thread
|
||||
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
T_ParamSpec = ParamSpec("T_ParamSpec")
|
||||
|
||||
|
||||
async def to_thread(
|
||||
func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
|
||||
) -> T_Retval:
|
||||
if sniffio.current_async_library() == "asyncio":
|
||||
return await asyncio.to_thread(func, *args, **kwargs)
|
||||
|
||||
return await anyio.to_thread.run_sync(
|
||||
functools.partial(func, *args, **kwargs),
|
||||
)
|
||||
|
||||
|
||||
# inspired by `asyncer`, https://github.com/tiangolo/asyncer
|
||||
def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
|
||||
"""
|
||||
Take a blocking function and create an async one that receives the same
|
||||
positional and keyword arguments.
|
||||
|
||||
Usage:
|
||||
|
||||
```python
|
||||
def blocking_func(arg1, arg2, kwarg1=None):
|
||||
# blocking code
|
||||
return result
|
||||
|
||||
|
||||
result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1)
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
`function`: a blocking regular callable (e.g. a function)
|
||||
|
||||
## Return
|
||||
|
||||
An async function that takes the same positional and keyword arguments as the
|
||||
original one, that when called runs the same original function in a thread worker
|
||||
and returns the result.
|
||||
"""
|
||||
|
||||
async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval:
|
||||
return await to_thread(function, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
@@ -0,0 +1,472 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import base64
|
||||
import pathlib
|
||||
from typing import Any, Mapping, TypeVar, cast
|
||||
from datetime import date, datetime
|
||||
from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints
|
||||
|
||||
import anyio
|
||||
import pydantic
|
||||
|
||||
from ._utils import (
|
||||
is_list,
|
||||
is_given,
|
||||
lru_cache,
|
||||
is_mapping,
|
||||
is_iterable,
|
||||
is_sequence,
|
||||
)
|
||||
from .._files import is_base64_file_input
|
||||
from ._compat import get_origin, is_typeddict
|
||||
from ._typing import (
|
||||
is_list_type,
|
||||
is_union_type,
|
||||
extract_type_arg,
|
||||
is_iterable_type,
|
||||
is_required_type,
|
||||
is_sequence_type,
|
||||
is_annotated_type,
|
||||
strip_annotated_type,
|
||||
)
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
# TODO: support for drilling globals() and locals()
|
||||
# TODO: ensure works correctly with forward references in all cases
|
||||
|
||||
|
||||
PropertyFormat = Literal["iso8601", "base64", "custom"]
|
||||
|
||||
|
||||
class PropertyInfo:
|
||||
"""Metadata class to be used in Annotated types to provide information about a given type.
|
||||
|
||||
For example:
|
||||
|
||||
class MyParams(TypedDict):
|
||||
account_holder_name: Annotated[str, PropertyInfo(alias='accountHolderName')]
|
||||
|
||||
This means that {'account_holder_name': 'Robert'} will be transformed to {'accountHolderName': 'Robert'} before being sent to the API.
|
||||
"""
|
||||
|
||||
alias: str | None
|
||||
format: PropertyFormat | None
|
||||
format_template: str | None
|
||||
discriminator: str | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
alias: str | None = None,
|
||||
format: PropertyFormat | None = None,
|
||||
format_template: str | None = None,
|
||||
discriminator: str | None = None,
|
||||
) -> None:
|
||||
self.alias = alias
|
||||
self.format = format
|
||||
self.format_template = format_template
|
||||
self.discriminator = discriminator
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}', discriminator='{self.discriminator}')"
|
||||
|
||||
|
||||
def maybe_transform(
|
||||
data: object,
|
||||
expected_type: object,
|
||||
) -> Any | None:
|
||||
"""Wrapper over `transform()` that allows `None` to be passed.
|
||||
|
||||
See `transform()` for more details.
|
||||
"""
|
||||
if data is None:
|
||||
return None
|
||||
return transform(data, expected_type)
|
||||
|
||||
|
||||
# Wrapper over _transform_recursive providing fake types
|
||||
def transform(
|
||||
data: _T,
|
||||
expected_type: object,
|
||||
) -> _T:
|
||||
"""Transform dictionaries based off of type information from the given type, for example:
|
||||
|
||||
```py
|
||||
class Params(TypedDict, total=False):
|
||||
card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]]
|
||||
|
||||
|
||||
transformed = transform({"card_id": "<my card ID>"}, Params)
|
||||
# {'cardID': '<my card ID>'}
|
||||
```
|
||||
|
||||
Any keys / data that does not have type information given will be included as is.
|
||||
|
||||
It should be noted that the transformations that this function does are not represented in the type system.
|
||||
"""
|
||||
transformed = _transform_recursive(data, annotation=cast(type, expected_type))
|
||||
return cast(_T, transformed)
|
||||
|
||||
|
||||
@lru_cache(maxsize=8096)
|
||||
def _get_annotated_type(type_: type) -> type | None:
|
||||
"""If the given type is an `Annotated` type then it is returned, if not `None` is returned.
|
||||
|
||||
This also unwraps the type when applicable, e.g. `Required[Annotated[T, ...]]`
|
||||
"""
|
||||
if is_required_type(type_):
|
||||
# Unwrap `Required[Annotated[T, ...]]` to `Annotated[T, ...]`
|
||||
type_ = get_args(type_)[0]
|
||||
|
||||
if is_annotated_type(type_):
|
||||
return type_
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _maybe_transform_key(key: str, type_: type) -> str:
|
||||
"""Transform the given `data` based on the annotations provided in `type_`.
|
||||
|
||||
Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata.
|
||||
"""
|
||||
annotated_type = _get_annotated_type(type_)
|
||||
if annotated_type is None:
|
||||
# no `Annotated` definition for this type, no transformation needed
|
||||
return key
|
||||
|
||||
# ignore the first argument as it is the actual type
|
||||
annotations = get_args(annotated_type)[1:]
|
||||
for annotation in annotations:
|
||||
if isinstance(annotation, PropertyInfo) and annotation.alias is not None:
|
||||
return annotation.alias
|
||||
|
||||
return key
|
||||
|
||||
|
||||
def _no_transform_needed(annotation: type) -> bool:
|
||||
return annotation == float or annotation == int
|
||||
|
||||
|
||||
def _transform_recursive(
|
||||
data: object,
|
||||
*,
|
||||
annotation: type,
|
||||
inner_type: type | None = None,
|
||||
) -> object:
|
||||
"""Transform the given data against the expected type.
|
||||
|
||||
Args:
|
||||
annotation: The direct type annotation given to the particular piece of data.
|
||||
This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc
|
||||
|
||||
inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type
|
||||
is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in
|
||||
the list can be transformed using the metadata from the container type.
|
||||
|
||||
Defaults to the same value as the `annotation` argument.
|
||||
"""
|
||||
from .._compat import model_dump
|
||||
|
||||
if inner_type is None:
|
||||
inner_type = annotation
|
||||
|
||||
stripped_type = strip_annotated_type(inner_type)
|
||||
origin = get_origin(stripped_type) or stripped_type
|
||||
if is_typeddict(stripped_type) and is_mapping(data):
|
||||
return _transform_typeddict(data, stripped_type)
|
||||
|
||||
if origin == dict and is_mapping(data):
|
||||
items_type = get_args(stripped_type)[1]
|
||||
return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()}
|
||||
|
||||
if (
|
||||
# List[T]
|
||||
(is_list_type(stripped_type) and is_list(data))
|
||||
# Iterable[T]
|
||||
or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
|
||||
# Sequence[T]
|
||||
or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str))
|
||||
):
|
||||
# dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
|
||||
# intended as an iterable, so we don't transform it.
|
||||
if isinstance(data, dict):
|
||||
return cast(object, data)
|
||||
|
||||
inner_type = extract_type_arg(stripped_type, 0)
|
||||
if _no_transform_needed(inner_type):
|
||||
# for some types there is no need to transform anything, so we can get a small
|
||||
# perf boost from skipping that work.
|
||||
#
|
||||
# but we still need to convert to a list to ensure the data is json-serializable
|
||||
if is_list(data):
|
||||
return data
|
||||
return list(data)
|
||||
|
||||
return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
|
||||
|
||||
if is_union_type(stripped_type):
|
||||
# For union types we run the transformation against all subtypes to ensure that everything is transformed.
|
||||
#
|
||||
# TODO: there may be edge cases where the same normalized field name will transform to two different names
|
||||
# in different subtypes.
|
||||
for subtype in get_args(stripped_type):
|
||||
data = _transform_recursive(data, annotation=annotation, inner_type=subtype)
|
||||
return data
|
||||
|
||||
if isinstance(data, pydantic.BaseModel):
|
||||
return model_dump(data, exclude_unset=True, mode="json")
|
||||
|
||||
annotated_type = _get_annotated_type(annotation)
|
||||
if annotated_type is None:
|
||||
return data
|
||||
|
||||
# ignore the first argument as it is the actual type
|
||||
annotations = get_args(annotated_type)[1:]
|
||||
for annotation in annotations:
|
||||
if isinstance(annotation, PropertyInfo) and annotation.format is not None:
|
||||
return _format_data(data, annotation.format, annotation.format_template)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object:
|
||||
if isinstance(data, (date, datetime)):
|
||||
if format_ == "iso8601":
|
||||
return data.isoformat()
|
||||
|
||||
if format_ == "custom" and format_template is not None:
|
||||
return data.strftime(format_template)
|
||||
|
||||
if format_ == "base64" and is_base64_file_input(data):
|
||||
binary: str | bytes | None = None
|
||||
|
||||
if isinstance(data, pathlib.Path):
|
||||
binary = data.read_bytes()
|
||||
elif isinstance(data, io.IOBase):
|
||||
binary = data.read()
|
||||
|
||||
if isinstance(binary, str): # type: ignore[unreachable]
|
||||
binary = binary.encode()
|
||||
|
||||
if not isinstance(binary, bytes):
|
||||
raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}")
|
||||
|
||||
return base64.b64encode(binary).decode("ascii")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _transform_typeddict(
|
||||
data: Mapping[str, object],
|
||||
expected_type: type,
|
||||
) -> Mapping[str, object]:
|
||||
result: dict[str, object] = {}
|
||||
annotations = get_type_hints(expected_type, include_extras=True)
|
||||
for key, value in data.items():
|
||||
if not is_given(value):
|
||||
# we don't need to include omitted values here as they'll
|
||||
# be stripped out before the request is sent anyway
|
||||
continue
|
||||
|
||||
type_ = annotations.get(key)
|
||||
if type_ is None:
|
||||
# we do not have a type annotation for this field, leave it as is
|
||||
result[key] = value
|
||||
else:
|
||||
result[_maybe_transform_key(key, type_)] = _transform_recursive(value, annotation=type_)
|
||||
return result
|
||||
|
||||
|
||||
async def async_maybe_transform(
|
||||
data: object,
|
||||
expected_type: object,
|
||||
) -> Any | None:
|
||||
"""Wrapper over `async_transform()` that allows `None` to be passed.
|
||||
|
||||
See `async_transform()` for more details.
|
||||
"""
|
||||
if data is None:
|
||||
return None
|
||||
return await async_transform(data, expected_type)
|
||||
|
||||
|
||||
async def async_transform(
|
||||
data: _T,
|
||||
expected_type: object,
|
||||
) -> _T:
|
||||
"""Transform dictionaries based off of type information from the given type, for example:
|
||||
|
||||
```py
|
||||
class Params(TypedDict, total=False):
|
||||
card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]]
|
||||
|
||||
|
||||
transformed = transform({"card_id": "<my card ID>"}, Params)
|
||||
# {'cardID': '<my card ID>'}
|
||||
```
|
||||
|
||||
Any keys / data that does not have type information given will be included as is.
|
||||
|
||||
It should be noted that the transformations that this function does are not represented in the type system.
|
||||
"""
|
||||
transformed = await _async_transform_recursive(data, annotation=cast(type, expected_type))
|
||||
return cast(_T, transformed)
|
||||
|
||||
|
||||
async def _async_transform_recursive(
|
||||
data: object,
|
||||
*,
|
||||
annotation: type,
|
||||
inner_type: type | None = None,
|
||||
) -> object:
|
||||
"""Transform the given data against the expected type.
|
||||
|
||||
Args:
|
||||
annotation: The direct type annotation given to the particular piece of data.
|
||||
This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc
|
||||
|
||||
inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type
|
||||
is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in
|
||||
the list can be transformed using the metadata from the container type.
|
||||
|
||||
Defaults to the same value as the `annotation` argument.
|
||||
"""
|
||||
from .._compat import model_dump
|
||||
|
||||
if inner_type is None:
|
||||
inner_type = annotation
|
||||
|
||||
stripped_type = strip_annotated_type(inner_type)
|
||||
origin = get_origin(stripped_type) or stripped_type
|
||||
if is_typeddict(stripped_type) and is_mapping(data):
|
||||
return await _async_transform_typeddict(data, stripped_type)
|
||||
|
||||
if origin == dict and is_mapping(data):
|
||||
items_type = get_args(stripped_type)[1]
|
||||
return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()}
|
||||
|
||||
if (
|
||||
# List[T]
|
||||
(is_list_type(stripped_type) and is_list(data))
|
||||
# Iterable[T]
|
||||
or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
|
||||
# Sequence[T]
|
||||
or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str))
|
||||
):
|
||||
# dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
|
||||
# intended as an iterable, so we don't transform it.
|
||||
if isinstance(data, dict):
|
||||
return cast(object, data)
|
||||
|
||||
inner_type = extract_type_arg(stripped_type, 0)
|
||||
if _no_transform_needed(inner_type):
|
||||
# for some types there is no need to transform anything, so we can get a small
|
||||
# perf boost from skipping that work.
|
||||
#
|
||||
# but we still need to convert to a list to ensure the data is json-serializable
|
||||
if is_list(data):
|
||||
return data
|
||||
return list(data)
|
||||
|
||||
return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
|
||||
|
||||
if is_union_type(stripped_type):
|
||||
# For union types we run the transformation against all subtypes to ensure that everything is transformed.
|
||||
#
|
||||
# TODO: there may be edge cases where the same normalized field name will transform to two different names
|
||||
# in different subtypes.
|
||||
for subtype in get_args(stripped_type):
|
||||
data = await _async_transform_recursive(data, annotation=annotation, inner_type=subtype)
|
||||
return data
|
||||
|
||||
if isinstance(data, pydantic.BaseModel):
|
||||
return model_dump(data, exclude_unset=True, mode="json")
|
||||
|
||||
annotated_type = _get_annotated_type(annotation)
|
||||
if annotated_type is None:
|
||||
return data
|
||||
|
||||
# ignore the first argument as it is the actual type
|
||||
annotations = get_args(annotated_type)[1:]
|
||||
for annotation in annotations:
|
||||
if isinstance(annotation, PropertyInfo) and annotation.format is not None:
|
||||
return await _async_format_data(data, annotation.format, annotation.format_template)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
async def _async_format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object:
|
||||
if isinstance(data, (date, datetime)):
|
||||
if format_ == "iso8601":
|
||||
return data.isoformat()
|
||||
|
||||
if format_ == "custom" and format_template is not None:
|
||||
return data.strftime(format_template)
|
||||
|
||||
if format_ == "base64" and is_base64_file_input(data):
|
||||
binary: str | bytes | None = None
|
||||
|
||||
if isinstance(data, pathlib.Path):
|
||||
binary = await anyio.Path(data).read_bytes()
|
||||
elif isinstance(data, io.IOBase):
|
||||
binary = data.read()
|
||||
|
||||
if isinstance(binary, str): # type: ignore[unreachable]
|
||||
binary = binary.encode()
|
||||
|
||||
if not isinstance(binary, bytes):
|
||||
raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}")
|
||||
|
||||
return base64.b64encode(binary).decode("ascii")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
async def _async_transform_typeddict(
|
||||
data: Mapping[str, object],
|
||||
expected_type: type,
|
||||
) -> Mapping[str, object]:
|
||||
result: dict[str, object] = {}
|
||||
annotations = get_type_hints(expected_type, include_extras=True)
|
||||
for key, value in data.items():
|
||||
if not is_given(value):
|
||||
# we don't need to include omitted values here as they'll
|
||||
# be stripped out before the request is sent anyway
|
||||
continue
|
||||
|
||||
type_ = annotations.get(key)
|
||||
if type_ is None:
|
||||
# we do not have a type annotation for this field, leave it as is
|
||||
result[key] = value
|
||||
else:
|
||||
result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_)
|
||||
return result
|
||||
|
||||
|
||||
@lru_cache(maxsize=8096)
|
||||
def get_type_hints(
|
||||
obj: Any,
|
||||
globalns: dict[str, Any] | None = None,
|
||||
localns: Mapping[str, Any] | None = None,
|
||||
include_extras: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras)
|
||||
@@ -0,0 +1,172 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import typing
|
||||
import typing_extensions
|
||||
from typing import Any, TypeVar, Iterable, cast
|
||||
from collections import abc as _c_abc
|
||||
from typing_extensions import (
|
||||
TypeIs,
|
||||
Required,
|
||||
Annotated,
|
||||
get_args,
|
||||
get_origin,
|
||||
)
|
||||
|
||||
from ._utils import lru_cache
|
||||
from .._types import InheritsGeneric
|
||||
from ._compat import is_union as _is_union
|
||||
|
||||
|
||||
def is_annotated_type(typ: type) -> bool:
|
||||
return get_origin(typ) == Annotated
|
||||
|
||||
|
||||
def is_list_type(typ: type) -> bool:
|
||||
return (get_origin(typ) or typ) == list
|
||||
|
||||
|
||||
def is_sequence_type(typ: type) -> bool:
|
||||
origin = get_origin(typ) or typ
|
||||
return origin == typing_extensions.Sequence or origin == typing.Sequence or origin == _c_abc.Sequence
|
||||
|
||||
|
||||
def is_iterable_type(typ: type) -> bool:
|
||||
"""If the given type is `typing.Iterable[T]`"""
|
||||
origin = get_origin(typ) or typ
|
||||
return origin == Iterable or origin == _c_abc.Iterable
|
||||
|
||||
|
||||
def is_union_type(typ: type) -> bool:
|
||||
return _is_union(get_origin(typ))
|
||||
|
||||
|
||||
def is_required_type(typ: type) -> bool:
|
||||
return get_origin(typ) == Required
|
||||
|
||||
|
||||
def is_typevar(typ: type) -> bool:
|
||||
# type ignore is required because type checkers
|
||||
# think this expression will always return False
|
||||
return type(typ) == TypeVar # type: ignore
|
||||
|
||||
|
||||
_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,)
|
||||
if sys.version_info >= (3, 12):
|
||||
_TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType)
|
||||
|
||||
|
||||
def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]:
|
||||
"""Return whether the provided argument is an instance of `TypeAliasType`.
|
||||
|
||||
```python
|
||||
type Int = int
|
||||
is_type_alias_type(Int)
|
||||
# > True
|
||||
Str = TypeAliasType("Str", str)
|
||||
is_type_alias_type(Str)
|
||||
# > True
|
||||
```
|
||||
"""
|
||||
return isinstance(tp, _TYPE_ALIAS_TYPES)
|
||||
|
||||
|
||||
# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]]
|
||||
@lru_cache(maxsize=8096)
|
||||
def strip_annotated_type(typ: type) -> type:
|
||||
if is_required_type(typ) or is_annotated_type(typ):
|
||||
return strip_annotated_type(cast(type, get_args(typ)[0]))
|
||||
|
||||
return typ
|
||||
|
||||
|
||||
def extract_type_arg(typ: type, index: int) -> type:
|
||||
args = get_args(typ)
|
||||
try:
|
||||
return cast(type, args[index])
|
||||
except IndexError as err:
|
||||
raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err
|
||||
|
||||
|
||||
def extract_type_var_from_base(
|
||||
typ: type,
|
||||
*,
|
||||
generic_bases: tuple[type, ...],
|
||||
index: int,
|
||||
failure_message: str | None = None,
|
||||
) -> type:
|
||||
"""Given a type like `Foo[T]`, returns the generic type variable `T`.
|
||||
|
||||
This also handles the case where a concrete subclass is given, e.g.
|
||||
```py
|
||||
class MyResponse(Foo[bytes]):
|
||||
...
|
||||
|
||||
extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes
|
||||
```
|
||||
|
||||
And where a generic subclass is given:
|
||||
```py
|
||||
_T = TypeVar('_T')
|
||||
class MyResponse(Foo[_T]):
|
||||
...
|
||||
|
||||
extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes
|
||||
```
|
||||
"""
|
||||
cls = cast(object, get_origin(typ) or typ)
|
||||
if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains]
|
||||
# we're given the class directly
|
||||
return extract_type_arg(typ, index)
|
||||
|
||||
# if a subclass is given
|
||||
# ---
|
||||
# this is needed as __orig_bases__ is not present in the typeshed stubs
|
||||
# because it is intended to be for internal use only, however there does
|
||||
# not seem to be a way to resolve generic TypeVars for inherited subclasses
|
||||
# without using it.
|
||||
if isinstance(cls, InheritsGeneric):
|
||||
target_base_class: Any | None = None
|
||||
for base in cls.__orig_bases__:
|
||||
if base.__origin__ in generic_bases:
|
||||
target_base_class = base
|
||||
break
|
||||
|
||||
if target_base_class is None:
|
||||
raise RuntimeError(
|
||||
"Could not find the generic base class;\n"
|
||||
"This should never happen;\n"
|
||||
f"Does {cls} inherit from one of {generic_bases} ?"
|
||||
)
|
||||
|
||||
extracted = extract_type_arg(target_base_class, index)
|
||||
if is_typevar(extracted):
|
||||
# If the extracted type argument is itself a type variable
|
||||
# then that means the subclass itself is generic, so we have
|
||||
# to resolve the type argument from the class itself, not
|
||||
# the base class.
|
||||
#
|
||||
# Note: if there is more than 1 type argument, the subclass could
|
||||
# change the ordering of the type arguments, this is not currently
|
||||
# supported.
|
||||
return extract_type_arg(typ, index)
|
||||
|
||||
return extracted
|
||||
|
||||
raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}")
|
||||
@@ -0,0 +1,438 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import inspect
|
||||
import functools
|
||||
from typing import (
|
||||
Any,
|
||||
Tuple,
|
||||
Mapping,
|
||||
TypeVar,
|
||||
Callable,
|
||||
Iterable,
|
||||
Sequence,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
from pathlib import Path
|
||||
from datetime import date, datetime
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
import sniffio
|
||||
|
||||
from .._types import Omit, NotGiven, FileTypes, HeadersLike
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...])
|
||||
_MappingT = TypeVar("_MappingT", bound=Mapping[str, object])
|
||||
_SequenceT = TypeVar("_SequenceT", bound=Sequence[object])
|
||||
CallableT = TypeVar("CallableT", bound=Callable[..., Any])
|
||||
|
||||
|
||||
def flatten(t: Iterable[Iterable[_T]]) -> list[_T]:
|
||||
return [item for sublist in t for item in sublist]
|
||||
|
||||
|
||||
def extract_files(
|
||||
# TODO: this needs to take Dict but variance issues.....
|
||||
# create protocol type ?
|
||||
query: Mapping[str, object],
|
||||
*,
|
||||
paths: Sequence[Sequence[str]],
|
||||
) -> list[tuple[str, FileTypes]]:
|
||||
"""Recursively extract files from the given dictionary based on specified paths.
|
||||
|
||||
A path may look like this ['foo', 'files', '<array>', 'data'].
|
||||
|
||||
Note: this mutates the given dictionary.
|
||||
"""
|
||||
files: list[tuple[str, FileTypes]] = []
|
||||
for path in paths:
|
||||
files.extend(_extract_items(query, path, index=0, flattened_key=None))
|
||||
return files
|
||||
|
||||
|
||||
def _extract_items(
|
||||
obj: object,
|
||||
path: Sequence[str],
|
||||
*,
|
||||
index: int,
|
||||
flattened_key: str | None,
|
||||
) -> list[tuple[str, FileTypes]]:
|
||||
try:
|
||||
key = path[index]
|
||||
except IndexError:
|
||||
if not is_given(obj):
|
||||
# no value was provided - we can safely ignore
|
||||
return []
|
||||
|
||||
# cyclical import
|
||||
from .._files import assert_is_file_content
|
||||
|
||||
# We have exhausted the path, return the entry we found.
|
||||
assert flattened_key is not None
|
||||
|
||||
if is_list(obj):
|
||||
files: list[tuple[str, FileTypes]] = []
|
||||
for entry in obj:
|
||||
assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "")
|
||||
files.append((flattened_key + "[]", cast(FileTypes, entry)))
|
||||
return files
|
||||
|
||||
assert_is_file_content(obj, key=flattened_key)
|
||||
return [(flattened_key, cast(FileTypes, obj))]
|
||||
|
||||
index += 1
|
||||
if is_dict(obj):
|
||||
try:
|
||||
# Remove the field if there are no more dict keys in the path,
|
||||
# only "<array>" traversal markers or end.
|
||||
if all(p == "<array>" for p in path[index:]):
|
||||
item = obj.pop(key)
|
||||
else:
|
||||
item = obj[key]
|
||||
except KeyError:
|
||||
# Key was not present in the dictionary, this is not indicative of an error
|
||||
# as the given path may not point to a required field. We also do not want
|
||||
# to enforce required fields as the API may differ from the spec in some cases.
|
||||
return []
|
||||
if flattened_key is None:
|
||||
flattened_key = key
|
||||
else:
|
||||
flattened_key += f"[{key}]"
|
||||
return _extract_items(
|
||||
item,
|
||||
path,
|
||||
index=index,
|
||||
flattened_key=flattened_key,
|
||||
)
|
||||
elif is_list(obj):
|
||||
if key != "<array>":
|
||||
return []
|
||||
|
||||
return flatten(
|
||||
[
|
||||
_extract_items(
|
||||
item,
|
||||
path,
|
||||
index=index,
|
||||
flattened_key=flattened_key + "[]" if flattened_key is not None else "[]",
|
||||
)
|
||||
for item in obj
|
||||
]
|
||||
)
|
||||
|
||||
# Something unexpected was passed, just ignore it.
|
||||
return []
|
||||
|
||||
|
||||
def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]:
|
||||
return not isinstance(obj, NotGiven) and not isinstance(obj, Omit)
|
||||
|
||||
|
||||
# Type safe methods for narrowing types with TypeVars.
|
||||
# The default narrowing for isinstance(obj, dict) is dict[unknown, unknown],
|
||||
# however this cause Pyright to rightfully report errors. As we know we don't
|
||||
# care about the contained types we can safely use `object` in its place.
|
||||
#
|
||||
# There are two separate functions defined, `is_*` and `is_*_t` for different use cases.
|
||||
# `is_*` is for when you're dealing with an unknown input
|
||||
# `is_*_t` is for when you're narrowing a known union type to a specific subset
|
||||
|
||||
|
||||
def is_tuple(obj: object) -> TypeGuard[tuple[object, ...]]:
|
||||
return isinstance(obj, tuple)
|
||||
|
||||
|
||||
def is_tuple_t(obj: _TupleT | object) -> TypeGuard[_TupleT]:
|
||||
return isinstance(obj, tuple)
|
||||
|
||||
|
||||
def is_sequence(obj: object) -> TypeGuard[Sequence[object]]:
|
||||
return isinstance(obj, Sequence)
|
||||
|
||||
|
||||
def is_sequence_t(obj: _SequenceT | object) -> TypeGuard[_SequenceT]:
|
||||
return isinstance(obj, Sequence)
|
||||
|
||||
|
||||
def is_mapping(obj: object) -> TypeGuard[Mapping[str, object]]:
|
||||
return isinstance(obj, Mapping)
|
||||
|
||||
|
||||
def is_mapping_t(obj: _MappingT | object) -> TypeGuard[_MappingT]:
|
||||
return isinstance(obj, Mapping)
|
||||
|
||||
|
||||
def is_dict(obj: object) -> TypeGuard[dict[object, object]]:
|
||||
return isinstance(obj, dict)
|
||||
|
||||
|
||||
def is_list(obj: object) -> TypeGuard[list[object]]:
|
||||
return isinstance(obj, list)
|
||||
|
||||
|
||||
def is_iterable(obj: object) -> TypeGuard[Iterable[object]]:
|
||||
return isinstance(obj, Iterable)
|
||||
|
||||
|
||||
def deepcopy_minimal(item: _T) -> _T:
|
||||
"""Minimal reimplementation of copy.deepcopy() that will only copy certain object types:
|
||||
|
||||
- mappings, e.g. `dict`
|
||||
- list
|
||||
|
||||
This is done for performance reasons.
|
||||
"""
|
||||
if is_mapping(item):
|
||||
return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()})
|
||||
if is_list(item):
|
||||
return cast(_T, [deepcopy_minimal(entry) for entry in item])
|
||||
return item
|
||||
|
||||
|
||||
# copied from https://github.com/Rapptz/RoboDanny
|
||||
def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str:
|
||||
size = len(seq)
|
||||
if size == 0:
|
||||
return ""
|
||||
|
||||
if size == 1:
|
||||
return seq[0]
|
||||
|
||||
if size == 2:
|
||||
return f"{seq[0]} {final} {seq[1]}"
|
||||
|
||||
return delim.join(seq[:-1]) + f" {final} {seq[-1]}"
|
||||
|
||||
|
||||
def quote(string: str) -> str:
|
||||
"""Add single quotation marks around the given string. Does *not* do any escaping."""
|
||||
return f"'{string}'"
|
||||
|
||||
|
||||
def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]:
|
||||
"""Decorator to enforce a given set of arguments or variants of arguments are passed to the decorated function.
|
||||
|
||||
Useful for enforcing runtime validation of overloaded functions.
|
||||
|
||||
Example usage:
|
||||
```py
|
||||
@overload
|
||||
def foo(*, a: str) -> str: ...
|
||||
|
||||
|
||||
@overload
|
||||
def foo(*, b: bool) -> str: ...
|
||||
|
||||
|
||||
# This enforces the same constraints that a static type checker would
|
||||
# i.e. that either a or b must be passed to the function
|
||||
@required_args(["a"], ["b"])
|
||||
def foo(*, a: str | None = None, b: bool | None = None) -> str: ...
|
||||
```
|
||||
"""
|
||||
|
||||
def inner(func: CallableT) -> CallableT:
|
||||
params = inspect.signature(func).parameters
|
||||
positional = [
|
||||
name
|
||||
for name, param in params.items()
|
||||
if param.kind
|
||||
in {
|
||||
param.POSITIONAL_ONLY,
|
||||
param.POSITIONAL_OR_KEYWORD,
|
||||
}
|
||||
]
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args: object, **kwargs: object) -> object:
|
||||
given_params: set[str] = set()
|
||||
for i, _ in enumerate(args):
|
||||
try:
|
||||
given_params.add(positional[i])
|
||||
except IndexError:
|
||||
raise TypeError(
|
||||
f"{func.__name__}() takes {len(positional)} argument(s) but {len(args)} were given"
|
||||
) from None
|
||||
|
||||
for key in kwargs.keys():
|
||||
given_params.add(key)
|
||||
|
||||
for variant in variants:
|
||||
matches = all((param in given_params for param in variant))
|
||||
if matches:
|
||||
break
|
||||
else: # no break
|
||||
if len(variants) > 1:
|
||||
variations = human_join(
|
||||
["(" + human_join([quote(arg) for arg in variant], final="and") + ")" for variant in variants]
|
||||
)
|
||||
msg = f"Missing required arguments; Expected either {variations} arguments to be given"
|
||||
else:
|
||||
assert len(variants) > 0
|
||||
|
||||
# TODO: this error message is not deterministic
|
||||
missing = list(set(variants[0]) - given_params)
|
||||
if len(missing) > 1:
|
||||
msg = f"Missing required arguments: {human_join([quote(arg) for arg in missing])}"
|
||||
else:
|
||||
msg = f"Missing required argument: {quote(missing[0])}"
|
||||
raise TypeError(msg)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper # type: ignore
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
_K = TypeVar("_K")
|
||||
_V = TypeVar("_V")
|
||||
|
||||
|
||||
@overload
|
||||
def strip_not_given(obj: None) -> None: ...
|
||||
|
||||
|
||||
@overload
|
||||
def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def strip_not_given(obj: object) -> object: ...
|
||||
|
||||
|
||||
def strip_not_given(obj: object | None) -> object:
|
||||
"""Remove all top-level keys where their values are instances of `NotGiven`"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not is_mapping(obj):
|
||||
return obj
|
||||
|
||||
return {key: value for key, value in obj.items() if not isinstance(value, NotGiven)}
|
||||
|
||||
|
||||
def coerce_integer(val: str) -> int:
|
||||
return int(val, base=10)
|
||||
|
||||
|
||||
def coerce_float(val: str) -> float:
|
||||
return float(val)
|
||||
|
||||
|
||||
def coerce_boolean(val: str) -> bool:
|
||||
return val == "true" or val == "1" or val == "on"
|
||||
|
||||
|
||||
def maybe_coerce_integer(val: str | None) -> int | None:
|
||||
if val is None:
|
||||
return None
|
||||
return coerce_integer(val)
|
||||
|
||||
|
||||
def maybe_coerce_float(val: str | None) -> float | None:
|
||||
if val is None:
|
||||
return None
|
||||
return coerce_float(val)
|
||||
|
||||
|
||||
def maybe_coerce_boolean(val: str | None) -> bool | None:
|
||||
if val is None:
|
||||
return None
|
||||
return coerce_boolean(val)
|
||||
|
||||
|
||||
def removeprefix(string: str, prefix: str) -> str:
|
||||
"""Remove a prefix from a string.
|
||||
|
||||
Backport of `str.removeprefix` for Python < 3.9
|
||||
"""
|
||||
if string.startswith(prefix):
|
||||
return string[len(prefix) :]
|
||||
return string
|
||||
|
||||
|
||||
def removesuffix(string: str, suffix: str) -> str:
|
||||
"""Remove a suffix from a string.
|
||||
|
||||
Backport of `str.removesuffix` for Python < 3.9
|
||||
"""
|
||||
if string.endswith(suffix):
|
||||
return string[: -len(suffix)]
|
||||
return string
|
||||
|
||||
|
||||
def file_from_path(path: str) -> FileTypes:
|
||||
contents = Path(path).read_bytes()
|
||||
file_name = os.path.basename(path)
|
||||
return (file_name, contents)
|
||||
|
||||
|
||||
def get_required_header(headers: HeadersLike, header: str) -> str:
|
||||
lower_header = header.lower()
|
||||
if is_mapping_t(headers):
|
||||
# mypy doesn't understand the type narrowing here
|
||||
for k, v in headers.items(): # type: ignore
|
||||
if k.lower() == lower_header and isinstance(v, str):
|
||||
return v
|
||||
|
||||
# to deal with the case where the header looks like Stainless-Event-Id
|
||||
intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize())
|
||||
|
||||
for normalized_header in [header, lower_header, header.upper(), intercaps_header]:
|
||||
value = headers.get(normalized_header)
|
||||
if value:
|
||||
return value
|
||||
|
||||
raise ValueError(f"Could not find {header} header")
|
||||
|
||||
|
||||
def get_async_library() -> str:
|
||||
try:
|
||||
return sniffio.current_async_library()
|
||||
except Exception:
|
||||
return "false"
|
||||
|
||||
|
||||
def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]:
|
||||
"""A version of functools.lru_cache that retains the type signature
|
||||
for the wrapped function arguments.
|
||||
"""
|
||||
wrapper = functools.lru_cache( # noqa: TID251
|
||||
maxsize=maxsize,
|
||||
)
|
||||
return cast(Any, wrapper) # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def json_safe(data: object) -> object:
|
||||
"""Translates a mapping / sequence recursively in the same fashion
|
||||
as `pydantic` v2's `model_dump(mode="json")`.
|
||||
"""
|
||||
if is_mapping(data):
|
||||
return {json_safe(key): json_safe(value) for key, value in data.items()}
|
||||
|
||||
if is_iterable(data) and not isinstance(data, (str, bytes, bytearray)):
|
||||
return [json_safe(item) for item in data]
|
||||
|
||||
if isinstance(data, (datetime, date)):
|
||||
return data.isoformat()
|
||||
|
||||
return data
|
||||
@@ -0,0 +1,18 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from ..version import __version__ as __version__
|
||||
|
||||
__title__ = "google.genai._interactions"
|
||||
@@ -0,0 +1,48 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from .webhooks import (
|
||||
WebhooksResource,
|
||||
AsyncWebhooksResource,
|
||||
WebhooksResourceWithRawResponse,
|
||||
AsyncWebhooksResourceWithRawResponse,
|
||||
WebhooksResourceWithStreamingResponse,
|
||||
AsyncWebhooksResourceWithStreamingResponse,
|
||||
)
|
||||
from .interactions import (
|
||||
InteractionsResource,
|
||||
AsyncInteractionsResource,
|
||||
InteractionsResourceWithRawResponse,
|
||||
AsyncInteractionsResourceWithRawResponse,
|
||||
InteractionsResourceWithStreamingResponse,
|
||||
AsyncInteractionsResourceWithStreamingResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"InteractionsResource",
|
||||
"AsyncInteractionsResource",
|
||||
"InteractionsResourceWithRawResponse",
|
||||
"AsyncInteractionsResourceWithRawResponse",
|
||||
"InteractionsResourceWithStreamingResponse",
|
||||
"AsyncInteractionsResourceWithStreamingResponse",
|
||||
"WebhooksResource",
|
||||
"AsyncWebhooksResource",
|
||||
"WebhooksResourceWithRawResponse",
|
||||
"AsyncWebhooksResourceWithRawResponse",
|
||||
"WebhooksResourceWithStreamingResponse",
|
||||
"AsyncWebhooksResourceWithStreamingResponse",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,135 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .tool import Tool as Tool
|
||||
from .turn import Turn as Turn
|
||||
from .model import Model as Model
|
||||
from .usage import Usage as Usage
|
||||
from .content import Content as Content
|
||||
from .webhook import Webhook as Webhook
|
||||
from .function import Function as Function
|
||||
from .annotation import Annotation as Annotation
|
||||
from .tool_param import ToolParam as ToolParam
|
||||
from .turn_param import TurnParam as TurnParam
|
||||
from .error_event import ErrorEvent as ErrorEvent
|
||||
from .interaction import Interaction as Interaction
|
||||
from .model_param import ModelParam as ModelParam
|
||||
from .usage_param import UsageParam as UsageParam
|
||||
from .content_stop import ContentStop as ContentStop
|
||||
from .image_config import ImageConfig as ImageConfig
|
||||
from .text_content import TextContent as TextContent
|
||||
from .url_citation import URLCitation as URLCitation
|
||||
from .allowed_tools import AllowedTools as AllowedTools
|
||||
from .audio_content import AudioContent as AudioContent
|
||||
from .content_delta import ContentDelta as ContentDelta
|
||||
from .content_param import ContentParam as ContentParam
|
||||
from .content_start import ContentStart as ContentStart
|
||||
from .file_citation import FileCitation as FileCitation
|
||||
from .image_content import ImageContent as ImageContent
|
||||
from .speech_config import SpeechConfig as SpeechConfig
|
||||
from .video_content import VideoContent as VideoContent
|
||||
from .function_param import FunctionParam as FunctionParam
|
||||
from .place_citation import PlaceCitation as PlaceCitation
|
||||
from .signing_secret import SigningSecret as SigningSecret
|
||||
from .thinking_level import ThinkingLevel as ThinkingLevel
|
||||
from .webhook_config import WebhookConfig as WebhookConfig
|
||||
from .thought_content import ThoughtContent as ThoughtContent
|
||||
from .annotation_param import AnnotationParam as AnnotationParam
|
||||
from .document_content import DocumentContent as DocumentContent
|
||||
from .tool_choice_type import ToolChoiceType as ToolChoiceType
|
||||
from .generation_config import GenerationConfig as GenerationConfig
|
||||
from .google_maps_result import GoogleMapsResult as GoogleMapsResult
|
||||
from .image_config_param import ImageConfigParam as ImageConfigParam
|
||||
from .text_content_param import TextContentParam as TextContentParam
|
||||
from .tool_choice_config import ToolChoiceConfig as ToolChoiceConfig
|
||||
from .url_citation_param import URLCitationParam as URLCitationParam
|
||||
from .url_context_result import URLContextResult as URLContextResult
|
||||
from .allowed_tools_param import AllowedToolsParam as AllowedToolsParam
|
||||
from .audio_content_param import AudioContentParam as AudioContentParam
|
||||
from .file_citation_param import FileCitationParam as FileCitationParam
|
||||
from .image_content_param import ImageContentParam as ImageContentParam
|
||||
from .speech_config_param import SpeechConfigParam as SpeechConfigParam
|
||||
from .video_content_param import VideoContentParam as VideoContentParam
|
||||
from .webhook_list_params import WebhookListParams as WebhookListParams
|
||||
from .webhook_ping_params import WebhookPingParams as WebhookPingParams
|
||||
from .dynamic_agent_config import DynamicAgentConfig as DynamicAgentConfig
|
||||
from .google_search_result import GoogleSearchResult as GoogleSearchResult
|
||||
from .place_citation_param import PlaceCitationParam as PlaceCitationParam
|
||||
from .webhook_config_param import WebhookConfigParam as WebhookConfigParam
|
||||
from .function_call_content import FunctionCallContent as FunctionCallContent
|
||||
from .interaction_sse_event import InteractionSSEEvent as InteractionSSEEvent
|
||||
from .thought_content_param import ThoughtContentParam as ThoughtContentParam
|
||||
from .webhook_create_params import WebhookCreateParams as WebhookCreateParams
|
||||
from .webhook_list_response import WebhookListResponse as WebhookListResponse
|
||||
from .webhook_ping_response import WebhookPingResponse as WebhookPingResponse
|
||||
from .webhook_update_params import WebhookUpdateParams as WebhookUpdateParams
|
||||
from .document_content_param import DocumentContentParam as DocumentContentParam
|
||||
from .interaction_get_params import InteractionGetParams as InteractionGetParams
|
||||
from .function_result_content import FunctionResultContent as FunctionResultContent
|
||||
from .generation_config_param import GenerationConfigParam as GenerationConfigParam
|
||||
from .interaction_start_event import InteractionStartEvent as InteractionStartEvent
|
||||
from .webhook_delete_response import WebhookDeleteResponse as WebhookDeleteResponse
|
||||
from .file_search_call_content import FileSearchCallContent as FileSearchCallContent
|
||||
from .google_maps_call_content import GoogleMapsCallContent as GoogleMapsCallContent
|
||||
from .google_maps_result_param import GoogleMapsResultParam as GoogleMapsResultParam
|
||||
from .tool_choice_config_param import ToolChoiceConfigParam as ToolChoiceConfigParam
|
||||
from .url_context_call_content import URLContextCallContent as URLContextCallContent
|
||||
from .url_context_result_param import URLContextResultParam as URLContextResultParam
|
||||
from .interaction_create_params import InteractionCreateParams as InteractionCreateParams
|
||||
from .interaction_status_update import InteractionStatusUpdate as InteractionStatusUpdate
|
||||
from .deep_research_agent_config import DeepResearchAgentConfig as DeepResearchAgentConfig
|
||||
from .dynamic_agent_config_param import DynamicAgentConfigParam as DynamicAgentConfigParam
|
||||
from .file_search_result_content import FileSearchResultContent as FileSearchResultContent
|
||||
from .google_maps_call_arguments import GoogleMapsCallArguments as GoogleMapsCallArguments
|
||||
from .google_maps_result_content import GoogleMapsResultContent as GoogleMapsResultContent
|
||||
from .google_search_call_content import GoogleSearchCallContent as GoogleSearchCallContent
|
||||
from .google_search_result_param import GoogleSearchResultParam as GoogleSearchResultParam
|
||||
from .interaction_complete_event import InteractionCompleteEvent as InteractionCompleteEvent
|
||||
from .url_context_call_arguments import URLContextCallArguments as URLContextCallArguments
|
||||
from .url_context_result_content import URLContextResultContent as URLContextResultContent
|
||||
from .code_execution_call_content import CodeExecutionCallContent as CodeExecutionCallContent
|
||||
from .function_call_content_param import FunctionCallContentParam as FunctionCallContentParam
|
||||
from .google_search_call_arguments import GoogleSearchCallArguments as GoogleSearchCallArguments
|
||||
from .google_search_result_content import GoogleSearchResultContent as GoogleSearchResultContent
|
||||
from .mcp_server_tool_call_content import MCPServerToolCallContent as MCPServerToolCallContent
|
||||
from .code_execution_call_arguments import CodeExecutionCallArguments as CodeExecutionCallArguments
|
||||
from .code_execution_result_content import CodeExecutionResultContent as CodeExecutionResultContent
|
||||
from .function_result_content_param import FunctionResultContentParam as FunctionResultContentParam
|
||||
from .file_search_call_content_param import FileSearchCallContentParam as FileSearchCallContentParam
|
||||
from .google_maps_call_content_param import GoogleMapsCallContentParam as GoogleMapsCallContentParam
|
||||
from .mcp_server_tool_result_content import MCPServerToolResultContent as MCPServerToolResultContent
|
||||
from .url_context_call_content_param import URLContextCallContentParam as URLContextCallContentParam
|
||||
from .deep_research_agent_config_param import DeepResearchAgentConfigParam as DeepResearchAgentConfigParam
|
||||
from .file_search_result_content_param import FileSearchResultContentParam as FileSearchResultContentParam
|
||||
from .google_maps_call_arguments_param import GoogleMapsCallArgumentsParam as GoogleMapsCallArgumentsParam
|
||||
from .google_maps_result_content_param import GoogleMapsResultContentParam as GoogleMapsResultContentParam
|
||||
from .google_search_call_content_param import GoogleSearchCallContentParam as GoogleSearchCallContentParam
|
||||
from .url_context_call_arguments_param import URLContextCallArgumentsParam as URLContextCallArgumentsParam
|
||||
from .url_context_result_content_param import URLContextResultContentParam as URLContextResultContentParam
|
||||
from .code_execution_call_content_param import CodeExecutionCallContentParam as CodeExecutionCallContentParam
|
||||
from .google_search_call_arguments_param import GoogleSearchCallArgumentsParam as GoogleSearchCallArgumentsParam
|
||||
from .google_search_result_content_param import GoogleSearchResultContentParam as GoogleSearchResultContentParam
|
||||
from .mcp_server_tool_call_content_param import MCPServerToolCallContentParam as MCPServerToolCallContentParam
|
||||
from .code_execution_call_arguments_param import CodeExecutionCallArgumentsParam as CodeExecutionCallArgumentsParam
|
||||
from .code_execution_result_content_param import CodeExecutionResultContentParam as CodeExecutionResultContentParam
|
||||
from .mcp_server_tool_result_content_param import MCPServerToolResultContentParam as MCPServerToolResultContentParam
|
||||
from .webhook_rotate_signing_secret_params import WebhookRotateSigningSecretParams as WebhookRotateSigningSecretParams
|
||||
from .webhook_rotate_signing_secret_response import (
|
||||
WebhookRotateSigningSecretResponse as WebhookRotateSigningSecretResponse,
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from .._models import BaseModel
|
||||
from .tool_choice_type import ToolChoiceType
|
||||
|
||||
__all__ = ["AllowedTools"]
|
||||
|
||||
|
||||
class AllowedTools(BaseModel):
|
||||
"""The configuration for allowed tools."""
|
||||
|
||||
mode: Optional[ToolChoiceType] = None
|
||||
"""The mode of the tool choice."""
|
||||
|
||||
tools: Optional[List[str]] = None
|
||||
"""The names of the allowed tools."""
|
||||
@@ -0,0 +1,35 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from .._types import SequenceNotStr
|
||||
from .tool_choice_type import ToolChoiceType
|
||||
|
||||
__all__ = ["AllowedToolsParam"]
|
||||
|
||||
|
||||
class AllowedToolsParam(TypedDict, total=False):
|
||||
"""The configuration for allowed tools."""
|
||||
|
||||
mode: ToolChoiceType
|
||||
"""The mode of the tool choice."""
|
||||
|
||||
tools: SequenceNotStr[str]
|
||||
"""The names of the allowed tools."""
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Union
|
||||
from typing_extensions import Annotated, TypeAlias
|
||||
|
||||
from .._utils import PropertyInfo
|
||||
from .url_citation import URLCitation
|
||||
from .file_citation import FileCitation
|
||||
from .place_citation import PlaceCitation
|
||||
|
||||
__all__ = ["Annotation"]
|
||||
|
||||
Annotation: TypeAlias = Annotated[Union[URLCitation, FileCitation, PlaceCitation], PropertyInfo(discriminator="type")]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from .url_citation_param import URLCitationParam
|
||||
from .file_citation_param import FileCitationParam
|
||||
from .place_citation_param import PlaceCitationParam
|
||||
|
||||
__all__ = ["AnnotationParam"]
|
||||
|
||||
AnnotationParam: TypeAlias = Union[URLCitationParam, FileCitationParam, PlaceCitationParam]
|
||||
@@ -0,0 +1,59 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["AudioContent"]
|
||||
|
||||
|
||||
class AudioContent(BaseModel):
|
||||
"""An audio content block."""
|
||||
|
||||
type: Literal["audio"]
|
||||
|
||||
channels: Optional[int] = None
|
||||
"""The number of audio channels."""
|
||||
|
||||
data: Optional[str] = None
|
||||
"""The audio content."""
|
||||
|
||||
mime_type: Optional[
|
||||
Literal[
|
||||
"audio/wav",
|
||||
"audio/mp3",
|
||||
"audio/aiff",
|
||||
"audio/aac",
|
||||
"audio/ogg",
|
||||
"audio/flac",
|
||||
"audio/mpeg",
|
||||
"audio/m4a",
|
||||
"audio/l16",
|
||||
"audio/opus",
|
||||
"audio/alaw",
|
||||
"audio/mulaw",
|
||||
]
|
||||
] = None
|
||||
"""The mime type of the audio."""
|
||||
|
||||
rate: Optional[int] = None
|
||||
"""The sample rate of the audio."""
|
||||
|
||||
uri: Optional[str] = None
|
||||
"""The URI of the audio."""
|
||||
@@ -0,0 +1,64 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
from typing_extensions import Literal, Required, Annotated, TypedDict
|
||||
|
||||
from .._types import Base64FileInput
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import set_pydantic_config
|
||||
|
||||
__all__ = ["AudioContentParam"]
|
||||
|
||||
|
||||
class AudioContentParam(TypedDict, total=False):
|
||||
"""An audio content block."""
|
||||
|
||||
type: Required[Literal["audio"]]
|
||||
|
||||
channels: int
|
||||
"""The number of audio channels."""
|
||||
|
||||
data: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
|
||||
"""The audio content."""
|
||||
|
||||
mime_type: Literal[
|
||||
"audio/wav",
|
||||
"audio/mp3",
|
||||
"audio/aiff",
|
||||
"audio/aac",
|
||||
"audio/ogg",
|
||||
"audio/flac",
|
||||
"audio/mpeg",
|
||||
"audio/m4a",
|
||||
"audio/l16",
|
||||
"audio/opus",
|
||||
"audio/alaw",
|
||||
"audio/mulaw",
|
||||
]
|
||||
"""The mime type of the audio."""
|
||||
|
||||
rate: int
|
||||
"""The sample rate of the audio."""
|
||||
|
||||
uri: str
|
||||
"""The URI of the audio."""
|
||||
|
||||
|
||||
set_pydantic_config(AudioContentParam, {"arbitrary_types_allowed": True})
|
||||
@@ -0,0 +1,33 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["CodeExecutionCallArguments"]
|
||||
|
||||
|
||||
class CodeExecutionCallArguments(BaseModel):
|
||||
"""The arguments to pass to the code execution."""
|
||||
|
||||
code: Optional[str] = None
|
||||
"""The code to be executed."""
|
||||
|
||||
language: Optional[Literal["python"]] = None
|
||||
"""Programming language of the `code`."""
|
||||
@@ -0,0 +1,32 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing_extensions import Literal, TypedDict
|
||||
|
||||
__all__ = ["CodeExecutionCallArgumentsParam"]
|
||||
|
||||
|
||||
class CodeExecutionCallArgumentsParam(TypedDict, total=False):
|
||||
"""The arguments to pass to the code execution."""
|
||||
|
||||
code: str
|
||||
"""The code to be executed."""
|
||||
|
||||
language: Literal["python"]
|
||||
"""Programming language of the `code`."""
|
||||
@@ -0,0 +1,39 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
from .code_execution_call_arguments import CodeExecutionCallArguments
|
||||
|
||||
__all__ = ["CodeExecutionCallContent"]
|
||||
|
||||
|
||||
class CodeExecutionCallContent(BaseModel):
|
||||
"""Code execution content."""
|
||||
|
||||
id: str
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
arguments: CodeExecutionCallArguments
|
||||
"""Required. The arguments to pass to the code execution."""
|
||||
|
||||
type: Literal["code_execution_call"]
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
@@ -0,0 +1,46 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
from typing_extensions import Literal, Required, Annotated, TypedDict
|
||||
|
||||
from .._types import Base64FileInput
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import set_pydantic_config
|
||||
from .code_execution_call_arguments_param import CodeExecutionCallArgumentsParam
|
||||
|
||||
__all__ = ["CodeExecutionCallContentParam"]
|
||||
|
||||
|
||||
class CodeExecutionCallContentParam(TypedDict, total=False):
|
||||
"""Code execution content."""
|
||||
|
||||
id: Required[str]
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
arguments: Required[CodeExecutionCallArgumentsParam]
|
||||
"""Required. The arguments to pass to the code execution."""
|
||||
|
||||
type: Required[Literal["code_execution_call"]]
|
||||
|
||||
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
set_pydantic_config(CodeExecutionCallContentParam, {"arbitrary_types_allowed": True})
|
||||
@@ -0,0 +1,41 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["CodeExecutionResultContent"]
|
||||
|
||||
|
||||
class CodeExecutionResultContent(BaseModel):
|
||||
"""Code execution result content."""
|
||||
|
||||
call_id: str
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: str
|
||||
"""Required. The output of the code execution."""
|
||||
|
||||
type: Literal["code_execution_result"]
|
||||
|
||||
is_error: Optional[bool] = None
|
||||
"""Whether the code execution resulted in an error."""
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
@@ -0,0 +1,48 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
from typing_extensions import Literal, Required, Annotated, TypedDict
|
||||
|
||||
from .._types import Base64FileInput
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import set_pydantic_config
|
||||
|
||||
__all__ = ["CodeExecutionResultContentParam"]
|
||||
|
||||
|
||||
class CodeExecutionResultContentParam(TypedDict, total=False):
|
||||
"""Code execution result content."""
|
||||
|
||||
call_id: Required[str]
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: Required[str]
|
||||
"""Required. The output of the code execution."""
|
||||
|
||||
type: Required[Literal["code_execution_result"]]
|
||||
|
||||
is_error: bool
|
||||
"""Whether the code execution resulted in an error."""
|
||||
|
||||
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
set_pydantic_config(CodeExecutionResultContentParam, {"arbitrary_types_allowed": True})
|
||||
@@ -0,0 +1,69 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Union
|
||||
from typing_extensions import Annotated, TypeAlias
|
||||
|
||||
from .._utils import PropertyInfo
|
||||
from .text_content import TextContent
|
||||
from .audio_content import AudioContent
|
||||
from .image_content import ImageContent
|
||||
from .video_content import VideoContent
|
||||
from .thought_content import ThoughtContent
|
||||
from .document_content import DocumentContent
|
||||
from .function_call_content import FunctionCallContent
|
||||
from .function_result_content import FunctionResultContent
|
||||
from .file_search_call_content import FileSearchCallContent
|
||||
from .google_maps_call_content import GoogleMapsCallContent
|
||||
from .url_context_call_content import URLContextCallContent
|
||||
from .file_search_result_content import FileSearchResultContent
|
||||
from .google_maps_result_content import GoogleMapsResultContent
|
||||
from .google_search_call_content import GoogleSearchCallContent
|
||||
from .url_context_result_content import URLContextResultContent
|
||||
from .code_execution_call_content import CodeExecutionCallContent
|
||||
from .google_search_result_content import GoogleSearchResultContent
|
||||
from .mcp_server_tool_call_content import MCPServerToolCallContent
|
||||
from .code_execution_result_content import CodeExecutionResultContent
|
||||
from .mcp_server_tool_result_content import MCPServerToolResultContent
|
||||
|
||||
__all__ = ["Content"]
|
||||
|
||||
Content: TypeAlias = Annotated[
|
||||
Union[
|
||||
TextContent,
|
||||
ImageContent,
|
||||
AudioContent,
|
||||
DocumentContent,
|
||||
VideoContent,
|
||||
ThoughtContent,
|
||||
FunctionCallContent,
|
||||
CodeExecutionCallContent,
|
||||
URLContextCallContent,
|
||||
MCPServerToolCallContent,
|
||||
GoogleSearchCallContent,
|
||||
FileSearchCallContent,
|
||||
GoogleMapsCallContent,
|
||||
FunctionResultContent,
|
||||
CodeExecutionResultContent,
|
||||
URLContextResultContent,
|
||||
GoogleSearchResultContent,
|
||||
MCPServerToolResultContent,
|
||||
FileSearchResultContent,
|
||||
GoogleMapsResultContent,
|
||||
],
|
||||
PropertyInfo(discriminator="type"),
|
||||
]
|
||||
@@ -0,0 +1,427 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Dict, List, Union, Optional
|
||||
from typing_extensions import Literal, Annotated, TypeAlias
|
||||
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import BaseModel
|
||||
from .annotation import Annotation
|
||||
from .text_content import TextContent
|
||||
from .image_content import ImageContent
|
||||
from .google_maps_result import GoogleMapsResult
|
||||
from .url_context_result import URLContextResult
|
||||
from .google_search_result import GoogleSearchResult
|
||||
from .google_maps_call_arguments import GoogleMapsCallArguments
|
||||
from .url_context_call_arguments import URLContextCallArguments
|
||||
from .google_search_call_arguments import GoogleSearchCallArguments
|
||||
from .code_execution_call_arguments import CodeExecutionCallArguments
|
||||
|
||||
__all__ = [
|
||||
"ContentDelta",
|
||||
"Delta",
|
||||
"DeltaText",
|
||||
"DeltaImage",
|
||||
"DeltaAudio",
|
||||
"DeltaDocument",
|
||||
"DeltaVideo",
|
||||
"DeltaThoughtSummary",
|
||||
"DeltaThoughtSummaryContent",
|
||||
"DeltaThoughtSignature",
|
||||
"DeltaFunctionCall",
|
||||
"DeltaCodeExecutionCall",
|
||||
"DeltaURLContextCall",
|
||||
"DeltaGoogleSearchCall",
|
||||
"DeltaMCPServerToolCall",
|
||||
"DeltaFileSearchCall",
|
||||
"DeltaGoogleMapsCall",
|
||||
"DeltaFunctionResult",
|
||||
"DeltaFunctionResultResultFunctionResultSubcontentList",
|
||||
"DeltaCodeExecutionResult",
|
||||
"DeltaURLContextResult",
|
||||
"DeltaGoogleSearchResult",
|
||||
"DeltaMCPServerToolResult",
|
||||
"DeltaMCPServerToolResultResultFunctionResultSubcontentList",
|
||||
"DeltaFileSearchResult",
|
||||
"DeltaFileSearchResultResult",
|
||||
"DeltaGoogleMapsResult",
|
||||
"DeltaTextAnnotation",
|
||||
]
|
||||
|
||||
|
||||
class DeltaText(BaseModel):
|
||||
text: str
|
||||
|
||||
type: Literal["text"]
|
||||
|
||||
|
||||
class DeltaImage(BaseModel):
|
||||
type: Literal["image"]
|
||||
|
||||
data: Optional[str] = None
|
||||
|
||||
mime_type: Optional[
|
||||
Literal[
|
||||
"image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff"
|
||||
]
|
||||
] = None
|
||||
|
||||
resolution: Optional[Literal["low", "medium", "high", "ultra_high"]] = None
|
||||
"""The resolution of the media."""
|
||||
|
||||
uri: Optional[str] = None
|
||||
|
||||
|
||||
class DeltaAudio(BaseModel):
|
||||
type: Literal["audio"]
|
||||
|
||||
channels: Optional[int] = None
|
||||
"""The number of audio channels."""
|
||||
|
||||
data: Optional[str] = None
|
||||
|
||||
mime_type: Optional[
|
||||
Literal[
|
||||
"audio/wav",
|
||||
"audio/mp3",
|
||||
"audio/aiff",
|
||||
"audio/aac",
|
||||
"audio/ogg",
|
||||
"audio/flac",
|
||||
"audio/mpeg",
|
||||
"audio/m4a",
|
||||
"audio/l16",
|
||||
"audio/opus",
|
||||
"audio/alaw",
|
||||
"audio/mulaw",
|
||||
]
|
||||
] = None
|
||||
|
||||
rate: Optional[int] = None
|
||||
"""The sample rate of the audio."""
|
||||
|
||||
uri: Optional[str] = None
|
||||
|
||||
|
||||
class DeltaDocument(BaseModel):
|
||||
type: Literal["document"]
|
||||
|
||||
data: Optional[str] = None
|
||||
|
||||
mime_type: Optional[Literal["application/pdf"]] = None
|
||||
|
||||
uri: Optional[str] = None
|
||||
|
||||
|
||||
class DeltaVideo(BaseModel):
|
||||
type: Literal["video"]
|
||||
|
||||
data: Optional[str] = None
|
||||
|
||||
mime_type: Optional[
|
||||
Literal[
|
||||
"video/mp4",
|
||||
"video/mpeg",
|
||||
"video/mpg",
|
||||
"video/mov",
|
||||
"video/avi",
|
||||
"video/x-flv",
|
||||
"video/webm",
|
||||
"video/wmv",
|
||||
"video/3gpp",
|
||||
]
|
||||
] = None
|
||||
|
||||
resolution: Optional[Literal["low", "medium", "high", "ultra_high"]] = None
|
||||
"""The resolution of the media."""
|
||||
|
||||
uri: Optional[str] = None
|
||||
|
||||
|
||||
DeltaThoughtSummaryContent: TypeAlias = Annotated[Union[TextContent, ImageContent], PropertyInfo(discriminator="type")]
|
||||
|
||||
|
||||
class DeltaThoughtSummary(BaseModel):
|
||||
type: Literal["thought_summary"]
|
||||
|
||||
content: Optional[DeltaThoughtSummaryContent] = None
|
||||
"""A new summary item to be added to the thought."""
|
||||
|
||||
|
||||
class DeltaThoughtSignature(BaseModel):
|
||||
type: Literal["thought_signature"]
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""Signature to match the backend source to be part of the generation."""
|
||||
|
||||
|
||||
class DeltaFunctionCall(BaseModel):
|
||||
id: str
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
arguments: Dict[str, object]
|
||||
|
||||
name: str
|
||||
|
||||
type: Literal["function_call"]
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
class DeltaCodeExecutionCall(BaseModel):
|
||||
id: str
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
arguments: CodeExecutionCallArguments
|
||||
"""The arguments to pass to the code execution."""
|
||||
|
||||
type: Literal["code_execution_call"]
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
class DeltaURLContextCall(BaseModel):
|
||||
id: str
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
arguments: URLContextCallArguments
|
||||
"""The arguments to pass to the URL context."""
|
||||
|
||||
type: Literal["url_context_call"]
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
class DeltaGoogleSearchCall(BaseModel):
|
||||
id: str
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
arguments: GoogleSearchCallArguments
|
||||
"""The arguments to pass to Google Search."""
|
||||
|
||||
type: Literal["google_search_call"]
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
class DeltaMCPServerToolCall(BaseModel):
|
||||
id: str
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
arguments: Dict[str, object]
|
||||
|
||||
name: str
|
||||
|
||||
server_name: str
|
||||
|
||||
type: Literal["mcp_server_tool_call"]
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
class DeltaFileSearchCall(BaseModel):
|
||||
id: str
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
type: Literal["file_search_call"]
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
class DeltaGoogleMapsCall(BaseModel):
|
||||
id: str
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
type: Literal["google_maps_call"]
|
||||
|
||||
arguments: Optional[GoogleMapsCallArguments] = None
|
||||
"""The arguments to pass to the Google Maps tool."""
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
DeltaFunctionResultResultFunctionResultSubcontentList: TypeAlias = Annotated[
|
||||
Union[TextContent, ImageContent], PropertyInfo(discriminator="type")
|
||||
]
|
||||
|
||||
|
||||
class DeltaFunctionResult(BaseModel):
|
||||
call_id: str
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: Union[List[DeltaFunctionResultResultFunctionResultSubcontentList], str, object]
|
||||
|
||||
type: Literal["function_result"]
|
||||
|
||||
is_error: Optional[bool] = None
|
||||
|
||||
name: Optional[str] = None
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
class DeltaCodeExecutionResult(BaseModel):
|
||||
call_id: str
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: str
|
||||
|
||||
type: Literal["code_execution_result"]
|
||||
|
||||
is_error: Optional[bool] = None
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
class DeltaURLContextResult(BaseModel):
|
||||
call_id: str
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: List[URLContextResult]
|
||||
|
||||
type: Literal["url_context_result"]
|
||||
|
||||
is_error: Optional[bool] = None
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
class DeltaGoogleSearchResult(BaseModel):
|
||||
call_id: str
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: List[GoogleSearchResult]
|
||||
|
||||
type: Literal["google_search_result"]
|
||||
|
||||
is_error: Optional[bool] = None
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
DeltaMCPServerToolResultResultFunctionResultSubcontentList: TypeAlias = Annotated[
|
||||
Union[TextContent, ImageContent], PropertyInfo(discriminator="type")
|
||||
]
|
||||
|
||||
|
||||
class DeltaMCPServerToolResult(BaseModel):
|
||||
call_id: str
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: Union[List[DeltaMCPServerToolResultResultFunctionResultSubcontentList], str, object]
|
||||
|
||||
type: Literal["mcp_server_tool_result"]
|
||||
|
||||
name: Optional[str] = None
|
||||
|
||||
server_name: Optional[str] = None
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
class DeltaFileSearchResultResult(BaseModel):
|
||||
"""The result of the File Search."""
|
||||
|
||||
custom_metadata: Optional[List[object]] = None
|
||||
"""User provided metadata about the FileSearchResult."""
|
||||
|
||||
|
||||
class DeltaFileSearchResult(BaseModel):
|
||||
call_id: str
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: List[DeltaFileSearchResultResult]
|
||||
|
||||
type: Literal["file_search_result"]
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
class DeltaGoogleMapsResult(BaseModel):
|
||||
call_id: str
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
type: Literal["google_maps_result"]
|
||||
|
||||
result: Optional[List[GoogleMapsResult]] = None
|
||||
"""The results of the Google Maps."""
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
class DeltaTextAnnotation(BaseModel):
|
||||
type: Literal["text_annotation"]
|
||||
|
||||
annotations: Optional[List[Annotation]] = None
|
||||
"""Citation information for model-generated content."""
|
||||
|
||||
|
||||
Delta: TypeAlias = Annotated[
|
||||
Union[
|
||||
DeltaText,
|
||||
DeltaImage,
|
||||
DeltaAudio,
|
||||
DeltaDocument,
|
||||
DeltaVideo,
|
||||
DeltaThoughtSummary,
|
||||
DeltaThoughtSignature,
|
||||
DeltaFunctionCall,
|
||||
DeltaCodeExecutionCall,
|
||||
DeltaURLContextCall,
|
||||
DeltaGoogleSearchCall,
|
||||
DeltaMCPServerToolCall,
|
||||
DeltaFileSearchCall,
|
||||
DeltaGoogleMapsCall,
|
||||
DeltaFunctionResult,
|
||||
DeltaCodeExecutionResult,
|
||||
DeltaURLContextResult,
|
||||
DeltaGoogleSearchResult,
|
||||
DeltaMCPServerToolResult,
|
||||
DeltaFileSearchResult,
|
||||
DeltaGoogleMapsResult,
|
||||
DeltaTextAnnotation,
|
||||
],
|
||||
PropertyInfo(discriminator="type"),
|
||||
]
|
||||
|
||||
|
||||
class ContentDelta(BaseModel):
|
||||
delta: Delta
|
||||
"""The delta content data for a content block."""
|
||||
|
||||
event_type: Literal["content.delta"]
|
||||
|
||||
index: int
|
||||
|
||||
event_id: Optional[str] = None
|
||||
"""
|
||||
The event_id token to be used to resume the interaction stream, from this event.
|
||||
"""
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from .text_content_param import TextContentParam
|
||||
from .audio_content_param import AudioContentParam
|
||||
from .image_content_param import ImageContentParam
|
||||
from .video_content_param import VideoContentParam
|
||||
from .thought_content_param import ThoughtContentParam
|
||||
from .document_content_param import DocumentContentParam
|
||||
from .function_call_content_param import FunctionCallContentParam
|
||||
from .function_result_content_param import FunctionResultContentParam
|
||||
from .file_search_call_content_param import FileSearchCallContentParam
|
||||
from .google_maps_call_content_param import GoogleMapsCallContentParam
|
||||
from .url_context_call_content_param import URLContextCallContentParam
|
||||
from .file_search_result_content_param import FileSearchResultContentParam
|
||||
from .google_maps_result_content_param import GoogleMapsResultContentParam
|
||||
from .google_search_call_content_param import GoogleSearchCallContentParam
|
||||
from .url_context_result_content_param import URLContextResultContentParam
|
||||
from .code_execution_call_content_param import CodeExecutionCallContentParam
|
||||
from .google_search_result_content_param import GoogleSearchResultContentParam
|
||||
from .mcp_server_tool_call_content_param import MCPServerToolCallContentParam
|
||||
from .code_execution_result_content_param import CodeExecutionResultContentParam
|
||||
from .mcp_server_tool_result_content_param import MCPServerToolResultContentParam
|
||||
|
||||
__all__ = ["ContentParam"]
|
||||
|
||||
ContentParam: TypeAlias = Union[
|
||||
TextContentParam,
|
||||
ImageContentParam,
|
||||
AudioContentParam,
|
||||
DocumentContentParam,
|
||||
VideoContentParam,
|
||||
ThoughtContentParam,
|
||||
FunctionCallContentParam,
|
||||
CodeExecutionCallContentParam,
|
||||
URLContextCallContentParam,
|
||||
MCPServerToolCallContentParam,
|
||||
GoogleSearchCallContentParam,
|
||||
FileSearchCallContentParam,
|
||||
GoogleMapsCallContentParam,
|
||||
FunctionResultContentParam,
|
||||
CodeExecutionResultContentParam,
|
||||
URLContextResultContentParam,
|
||||
GoogleSearchResultContentParam,
|
||||
MCPServerToolResultContentParam,
|
||||
FileSearchResultContentParam,
|
||||
GoogleMapsResultContentParam,
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .content import Content
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["ContentStart"]
|
||||
|
||||
|
||||
class ContentStart(BaseModel):
|
||||
content: Content
|
||||
"""The content of the response."""
|
||||
|
||||
event_type: Literal["content.start"]
|
||||
|
||||
index: int
|
||||
|
||||
event_id: Optional[str] = None
|
||||
"""
|
||||
The event_id token to be used to resume the interaction stream, from this event.
|
||||
"""
|
||||
@@ -0,0 +1,34 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["ContentStop"]
|
||||
|
||||
|
||||
class ContentStop(BaseModel):
|
||||
event_type: Literal["content.stop"]
|
||||
|
||||
index: int
|
||||
|
||||
event_id: Optional[str] = None
|
||||
"""
|
||||
The event_id token to be used to resume the interaction stream, from this event.
|
||||
"""
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["DeepResearchAgentConfig"]
|
||||
|
||||
|
||||
class DeepResearchAgentConfig(BaseModel):
|
||||
"""Configuration for the Deep Research agent."""
|
||||
|
||||
type: Literal["deep-research"]
|
||||
|
||||
collaborative_planning: Optional[bool] = None
|
||||
"""Enables human-in-the-loop planning for the Deep Research agent.
|
||||
|
||||
If set to true, the Deep Research agent will provide a research plan in its
|
||||
response. The agent will then proceed only if the user confirms the plan in the
|
||||
next turn. Relevant issue: b/482352502.
|
||||
"""
|
||||
|
||||
thinking_summaries: Optional[Literal["auto", "none"]] = None
|
||||
"""Whether to include thought summaries in the response."""
|
||||
|
||||
visualization: Optional[Literal["off", "auto"]] = None
|
||||
"""Whether to include visualizations in the response."""
|
||||
@@ -0,0 +1,42 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing_extensions import Literal, Required, TypedDict
|
||||
|
||||
__all__ = ["DeepResearchAgentConfigParam"]
|
||||
|
||||
|
||||
class DeepResearchAgentConfigParam(TypedDict, total=False):
|
||||
"""Configuration for the Deep Research agent."""
|
||||
|
||||
type: Required[Literal["deep-research"]]
|
||||
|
||||
collaborative_planning: bool
|
||||
"""Enables human-in-the-loop planning for the Deep Research agent.
|
||||
|
||||
If set to true, the Deep Research agent will provide a research plan in its
|
||||
response. The agent will then proceed only if the user confirms the plan in the
|
||||
next turn. Relevant issue: b/482352502.
|
||||
"""
|
||||
|
||||
thinking_summaries: Literal["auto", "none"]
|
||||
"""Whether to include thought summaries in the response."""
|
||||
|
||||
visualization: Literal["off", "auto"]
|
||||
"""Whether to include visualizations in the response."""
|
||||
@@ -0,0 +1,38 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["DocumentContent"]
|
||||
|
||||
|
||||
class DocumentContent(BaseModel):
|
||||
"""A document content block."""
|
||||
|
||||
type: Literal["document"]
|
||||
|
||||
data: Optional[str] = None
|
||||
"""The document content."""
|
||||
|
||||
mime_type: Optional[Literal["application/pdf"]] = None
|
||||
"""The mime type of the document."""
|
||||
|
||||
uri: Optional[str] = None
|
||||
"""The URI of the document."""
|
||||
@@ -0,0 +1,45 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
from typing_extensions import Literal, Required, Annotated, TypedDict
|
||||
|
||||
from .._types import Base64FileInput
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import set_pydantic_config
|
||||
|
||||
__all__ = ["DocumentContentParam"]
|
||||
|
||||
|
||||
class DocumentContentParam(TypedDict, total=False):
|
||||
"""A document content block."""
|
||||
|
||||
type: Required[Literal["document"]]
|
||||
|
||||
data: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
|
||||
"""The document content."""
|
||||
|
||||
mime_type: Literal["application/pdf"]
|
||||
"""The mime type of the document."""
|
||||
|
||||
uri: str
|
||||
"""The URI of the document."""
|
||||
|
||||
|
||||
set_pydantic_config(DocumentContentParam, {"arbitrary_types_allowed": True})
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
from typing_extensions import Literal
|
||||
|
||||
from pydantic import Field as FieldInfo
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["DynamicAgentConfig"]
|
||||
|
||||
|
||||
class DynamicAgentConfig(BaseModel):
|
||||
"""Configuration for dynamic agents."""
|
||||
|
||||
type: Literal["dynamic"]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
|
||||
# value to this field, so for compatibility we avoid doing it at runtime.
|
||||
__pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
|
||||
|
||||
# Stub to indicate that arbitrary properties are accepted.
|
||||
# To access properties that are not valid identifiers you can use `getattr`, e.g.
|
||||
# `getattr(obj, '$type')`
|
||||
def __getattr__(self, attr: str) -> object: ...
|
||||
else:
|
||||
__pydantic_extra__: Dict[str, object]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing_extensions import Literal, Required, TypedDict
|
||||
|
||||
__all__ = ["DynamicAgentConfigParam"]
|
||||
|
||||
|
||||
class DynamicAgentConfigParam(TypedDict, total=False, extra_items=object): # type: ignore[call-arg]
|
||||
"""Configuration for dynamic agents."""
|
||||
|
||||
type: Required[Literal["dynamic"]]
|
||||
@@ -0,0 +1,45 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["ErrorEvent", "Error"]
|
||||
|
||||
|
||||
class Error(BaseModel):
|
||||
"""Error message from an interaction."""
|
||||
|
||||
code: Optional[str] = None
|
||||
"""A URI that identifies the error type."""
|
||||
|
||||
message: Optional[str] = None
|
||||
"""A human-readable error message."""
|
||||
|
||||
|
||||
class ErrorEvent(BaseModel):
|
||||
event_type: Literal["error"]
|
||||
|
||||
error: Optional[Error] = None
|
||||
"""Error message from an interaction."""
|
||||
|
||||
event_id: Optional[str] = None
|
||||
"""
|
||||
The event_id token to be used to resume the interaction stream, from this event.
|
||||
"""
|
||||
@@ -0,0 +1,47 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["FileCitation"]
|
||||
|
||||
|
||||
class FileCitation(BaseModel):
|
||||
"""A file citation annotation."""
|
||||
|
||||
type: Literal["file_citation"]
|
||||
|
||||
document_uri: Optional[str] = None
|
||||
"""The URI of the file."""
|
||||
|
||||
end_index: Optional[int] = None
|
||||
"""End of the attributed segment, exclusive."""
|
||||
|
||||
file_name: Optional[str] = None
|
||||
"""The name of the file."""
|
||||
|
||||
source: Optional[str] = None
|
||||
"""Source attributed for a portion of the text."""
|
||||
|
||||
start_index: Optional[int] = None
|
||||
"""Start of segment of the response that is attributed to this source.
|
||||
|
||||
Index indicates the start of the segment, measured in bytes.
|
||||
"""
|
||||
@@ -0,0 +1,46 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing_extensions import Literal, Required, TypedDict
|
||||
|
||||
__all__ = ["FileCitationParam"]
|
||||
|
||||
|
||||
class FileCitationParam(TypedDict, total=False):
|
||||
"""A file citation annotation."""
|
||||
|
||||
type: Required[Literal["file_citation"]]
|
||||
|
||||
document_uri: str
|
||||
"""The URI of the file."""
|
||||
|
||||
end_index: int
|
||||
"""End of the attributed segment, exclusive."""
|
||||
|
||||
file_name: str
|
||||
"""The name of the file."""
|
||||
|
||||
source: str
|
||||
"""Source attributed for a portion of the text."""
|
||||
|
||||
start_index: int
|
||||
"""Start of segment of the response that is attributed to this source.
|
||||
|
||||
Index indicates the start of the segment, measured in bytes.
|
||||
"""
|
||||
@@ -0,0 +1,35 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["FileSearchCallContent"]
|
||||
|
||||
|
||||
class FileSearchCallContent(BaseModel):
|
||||
"""File Search content."""
|
||||
|
||||
id: str
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
type: Literal["file_search_call"]
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
@@ -0,0 +1,42 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
from typing_extensions import Literal, Required, Annotated, TypedDict
|
||||
|
||||
from .._types import Base64FileInput
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import set_pydantic_config
|
||||
|
||||
__all__ = ["FileSearchCallContentParam"]
|
||||
|
||||
|
||||
class FileSearchCallContentParam(TypedDict, total=False):
|
||||
"""File Search content."""
|
||||
|
||||
id: Required[str]
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
type: Required[Literal["file_search_call"]]
|
||||
|
||||
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
set_pydantic_config(FileSearchCallContentParam, {"arbitrary_types_allowed": True})
|
||||
@@ -0,0 +1,45 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import List, Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["FileSearchResultContent", "Result"]
|
||||
|
||||
|
||||
class Result(BaseModel):
|
||||
"""The result of the File Search."""
|
||||
|
||||
custom_metadata: Optional[List[object]] = None
|
||||
"""User provided metadata about the FileSearchResult."""
|
||||
|
||||
|
||||
class FileSearchResultContent(BaseModel):
|
||||
"""File Search result content."""
|
||||
|
||||
call_id: str
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: List[Result]
|
||||
"""Required. The results of the File Search."""
|
||||
|
||||
type: Literal["file_search_result"]
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
@@ -0,0 +1,52 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union, Iterable
|
||||
from typing_extensions import Literal, Required, Annotated, TypedDict
|
||||
|
||||
from .._types import Base64FileInput
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import set_pydantic_config
|
||||
|
||||
__all__ = ["FileSearchResultContentParam", "Result"]
|
||||
|
||||
|
||||
class Result(TypedDict, total=False):
|
||||
"""The result of the File Search."""
|
||||
|
||||
custom_metadata: Iterable[object]
|
||||
"""User provided metadata about the FileSearchResult."""
|
||||
|
||||
|
||||
class FileSearchResultContentParam(TypedDict, total=False):
|
||||
"""File Search result content."""
|
||||
|
||||
call_id: Required[str]
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: Required[Iterable[Result]]
|
||||
"""Required. The results of the File Search."""
|
||||
|
||||
type: Required[Literal["file_search_result"]]
|
||||
|
||||
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
set_pydantic_config(FileSearchResultContentParam, {"arbitrary_types_allowed": True})
|
||||
@@ -0,0 +1,38 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["Function"]
|
||||
|
||||
|
||||
class Function(BaseModel):
|
||||
"""A tool that can be used by the model."""
|
||||
|
||||
type: Literal["function"]
|
||||
|
||||
description: Optional[str] = None
|
||||
"""A description of the function."""
|
||||
|
||||
name: Optional[str] = None
|
||||
"""The name of the function."""
|
||||
|
||||
parameters: Optional[object] = None
|
||||
"""The JSON Schema for the function's parameters."""
|
||||
@@ -0,0 +1,41 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Dict, Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["FunctionCallContent"]
|
||||
|
||||
|
||||
class FunctionCallContent(BaseModel):
|
||||
"""A function tool call content block."""
|
||||
|
||||
id: str
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
arguments: Dict[str, object]
|
||||
"""Required. The arguments to pass to the function."""
|
||||
|
||||
name: str
|
||||
"""Required. The name of the tool to call."""
|
||||
|
||||
type: Literal["function_call"]
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
@@ -0,0 +1,48 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Union
|
||||
from typing_extensions import Literal, Required, Annotated, TypedDict
|
||||
|
||||
from .._types import Base64FileInput
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import set_pydantic_config
|
||||
|
||||
__all__ = ["FunctionCallContentParam"]
|
||||
|
||||
|
||||
class FunctionCallContentParam(TypedDict, total=False):
|
||||
"""A function tool call content block."""
|
||||
|
||||
id: Required[str]
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
arguments: Required[Dict[str, object]]
|
||||
"""Required. The arguments to pass to the function."""
|
||||
|
||||
name: Required[str]
|
||||
"""Required. The name of the tool to call."""
|
||||
|
||||
type: Required[Literal["function_call"]]
|
||||
|
||||
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
set_pydantic_config(FunctionCallContentParam, {"arbitrary_types_allowed": True})
|
||||
@@ -0,0 +1,37 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing_extensions import Literal, Required, TypedDict
|
||||
|
||||
__all__ = ["FunctionParam"]
|
||||
|
||||
|
||||
class FunctionParam(TypedDict, total=False):
|
||||
"""A tool that can be used by the model."""
|
||||
|
||||
type: Required[Literal["function"]]
|
||||
|
||||
description: str
|
||||
"""A description of the function."""
|
||||
|
||||
name: str
|
||||
"""The name of the function."""
|
||||
|
||||
parameters: object
|
||||
"""The JSON Schema for the function's parameters."""
|
||||
@@ -0,0 +1,51 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import List, Union, Optional
|
||||
from typing_extensions import Literal, Annotated, TypeAlias
|
||||
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import BaseModel
|
||||
from .text_content import TextContent
|
||||
from .image_content import ImageContent
|
||||
|
||||
__all__ = ["FunctionResultContent", "ResultFunctionResultSubcontentList"]
|
||||
|
||||
ResultFunctionResultSubcontentList: TypeAlias = Annotated[
|
||||
Union[TextContent, ImageContent], PropertyInfo(discriminator="type")
|
||||
]
|
||||
|
||||
|
||||
class FunctionResultContent(BaseModel):
|
||||
"""A function tool result content block."""
|
||||
|
||||
call_id: str
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: Union[List[ResultFunctionResultSubcontentList], str, object]
|
||||
"""The result of the tool call."""
|
||||
|
||||
type: Literal["function_result"]
|
||||
|
||||
is_error: Optional[bool] = None
|
||||
"""Whether the tool call resulted in an error."""
|
||||
|
||||
name: Optional[str] = None
|
||||
"""The name of the tool that was called."""
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
@@ -0,0 +1,55 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union, Iterable
|
||||
from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict
|
||||
|
||||
from .._types import Base64FileInput
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import set_pydantic_config
|
||||
from .text_content_param import TextContentParam
|
||||
from .image_content_param import ImageContentParam
|
||||
|
||||
__all__ = ["FunctionResultContentParam", "ResultFunctionResultSubcontentList"]
|
||||
|
||||
ResultFunctionResultSubcontentList: TypeAlias = Union[TextContentParam, ImageContentParam]
|
||||
|
||||
|
||||
class FunctionResultContentParam(TypedDict, total=False):
|
||||
"""A function tool result content block."""
|
||||
|
||||
call_id: Required[str]
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: Required[Union[Iterable[ResultFunctionResultSubcontentList], str, object]]
|
||||
"""The result of the tool call."""
|
||||
|
||||
type: Required[Literal["function_result"]]
|
||||
|
||||
is_error: bool
|
||||
"""Whether the tool call resulted in an error."""
|
||||
|
||||
name: str
|
||||
"""The name of the tool that was called."""
|
||||
|
||||
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
set_pydantic_config(FunctionResultContentParam, {"arbitrary_types_allowed": True})
|
||||
@@ -0,0 +1,64 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import List, Union, Optional
|
||||
from typing_extensions import Literal, TypeAlias
|
||||
|
||||
from .._models import BaseModel
|
||||
from .image_config import ImageConfig
|
||||
from .speech_config import SpeechConfig
|
||||
from .thinking_level import ThinkingLevel
|
||||
from .tool_choice_type import ToolChoiceType
|
||||
from .tool_choice_config import ToolChoiceConfig
|
||||
|
||||
__all__ = ["GenerationConfig", "ToolChoice"]
|
||||
|
||||
ToolChoice: TypeAlias = Union[ToolChoiceType, ToolChoiceConfig]
|
||||
|
||||
|
||||
class GenerationConfig(BaseModel):
|
||||
"""Configuration parameters for model interactions."""
|
||||
|
||||
image_config: Optional[ImageConfig] = None
|
||||
"""Configuration for image interaction."""
|
||||
|
||||
max_output_tokens: Optional[int] = None
|
||||
"""The maximum number of tokens to include in the response."""
|
||||
|
||||
seed: Optional[int] = None
|
||||
"""Seed used in decoding for reproducibility."""
|
||||
|
||||
speech_config: Optional[List[SpeechConfig]] = None
|
||||
"""Configuration for speech interaction."""
|
||||
|
||||
stop_sequences: Optional[List[str]] = None
|
||||
"""A list of character sequences that will stop output interaction."""
|
||||
|
||||
temperature: Optional[float] = None
|
||||
"""Controls the randomness of the output."""
|
||||
|
||||
thinking_level: Optional[ThinkingLevel] = None
|
||||
"""The level of thought tokens that the model should generate."""
|
||||
|
||||
thinking_summaries: Optional[Literal["auto", "none"]] = None
|
||||
"""Whether to include thought summaries in the response."""
|
||||
|
||||
tool_choice: Optional[ToolChoice] = None
|
||||
"""The tool choice configuration."""
|
||||
|
||||
top_p: Optional[float] = None
|
||||
"""The maximum cumulative probability of tokens to consider when sampling."""
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union, Iterable
|
||||
from typing_extensions import Literal, TypeAlias, TypedDict
|
||||
|
||||
from .._types import SequenceNotStr
|
||||
from .thinking_level import ThinkingLevel
|
||||
from .tool_choice_type import ToolChoiceType
|
||||
from .image_config_param import ImageConfigParam
|
||||
from .speech_config_param import SpeechConfigParam
|
||||
from .tool_choice_config_param import ToolChoiceConfigParam
|
||||
|
||||
__all__ = ["GenerationConfigParam", "ToolChoice"]
|
||||
|
||||
ToolChoice: TypeAlias = Union[ToolChoiceType, ToolChoiceConfigParam]
|
||||
|
||||
|
||||
class GenerationConfigParam(TypedDict, total=False):
|
||||
"""Configuration parameters for model interactions."""
|
||||
|
||||
image_config: ImageConfigParam
|
||||
"""Configuration for image interaction."""
|
||||
|
||||
max_output_tokens: int
|
||||
"""The maximum number of tokens to include in the response."""
|
||||
|
||||
seed: int
|
||||
"""Seed used in decoding for reproducibility."""
|
||||
|
||||
speech_config: Iterable[SpeechConfigParam]
|
||||
"""Configuration for speech interaction."""
|
||||
|
||||
stop_sequences: SequenceNotStr[str]
|
||||
"""A list of character sequences that will stop output interaction."""
|
||||
|
||||
temperature: float
|
||||
"""Controls the randomness of the output."""
|
||||
|
||||
thinking_level: ThinkingLevel
|
||||
"""The level of thought tokens that the model should generate."""
|
||||
|
||||
thinking_summaries: Literal["auto", "none"]
|
||||
"""Whether to include thought summaries in the response."""
|
||||
|
||||
tool_choice: ToolChoice
|
||||
"""The tool choice configuration."""
|
||||
|
||||
top_p: float
|
||||
"""The maximum cumulative probability of tokens to consider when sampling."""
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["GoogleMapsCallArguments"]
|
||||
|
||||
|
||||
class GoogleMapsCallArguments(BaseModel):
|
||||
"""The arguments to pass to the Google Maps tool."""
|
||||
|
||||
queries: Optional[List[str]] = None
|
||||
"""The queries to be executed."""
|
||||
@@ -0,0 +1,31 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from .._types import SequenceNotStr
|
||||
|
||||
__all__ = ["GoogleMapsCallArgumentsParam"]
|
||||
|
||||
|
||||
class GoogleMapsCallArgumentsParam(TypedDict, total=False):
|
||||
"""The arguments to pass to the Google Maps tool."""
|
||||
|
||||
queries: SequenceNotStr[str]
|
||||
"""The queries to be executed."""
|
||||
@@ -0,0 +1,39 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
from .google_maps_call_arguments import GoogleMapsCallArguments
|
||||
|
||||
__all__ = ["GoogleMapsCallContent"]
|
||||
|
||||
|
||||
class GoogleMapsCallContent(BaseModel):
|
||||
"""Google Maps content."""
|
||||
|
||||
id: str
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
type: Literal["google_maps_call"]
|
||||
|
||||
arguments: Optional[GoogleMapsCallArguments] = None
|
||||
"""The arguments to pass to the Google Maps tool."""
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
@@ -0,0 +1,46 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
from typing_extensions import Literal, Required, Annotated, TypedDict
|
||||
|
||||
from .._types import Base64FileInput
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import set_pydantic_config
|
||||
from .google_maps_call_arguments_param import GoogleMapsCallArgumentsParam
|
||||
|
||||
__all__ = ["GoogleMapsCallContentParam"]
|
||||
|
||||
|
||||
class GoogleMapsCallContentParam(TypedDict, total=False):
|
||||
"""Google Maps content."""
|
||||
|
||||
id: Required[str]
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
type: Required[Literal["google_maps_call"]]
|
||||
|
||||
arguments: GoogleMapsCallArgumentsParam
|
||||
"""The arguments to pass to the Google Maps tool."""
|
||||
|
||||
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
set_pydantic_config(GoogleMapsCallContentParam, {"arbitrary_types_allowed": True})
|
||||
@@ -0,0 +1,65 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["GoogleMapsResult", "Place", "PlaceReviewSnippet"]
|
||||
|
||||
|
||||
class PlaceReviewSnippet(BaseModel):
|
||||
"""
|
||||
Encapsulates a snippet of a user review that answers a question about
|
||||
the features of a specific place in Google Maps.
|
||||
"""
|
||||
|
||||
review_id: Optional[str] = None
|
||||
"""The ID of the review snippet."""
|
||||
|
||||
title: Optional[str] = None
|
||||
"""Title of the review."""
|
||||
|
||||
url: Optional[str] = None
|
||||
"""A link that corresponds to the user review on Google Maps."""
|
||||
|
||||
|
||||
class Place(BaseModel):
|
||||
name: Optional[str] = None
|
||||
"""Title of the place."""
|
||||
|
||||
place_id: Optional[str] = None
|
||||
"""The ID of the place, in `places/{place_id}` format."""
|
||||
|
||||
review_snippets: Optional[List[PlaceReviewSnippet]] = None
|
||||
"""
|
||||
Snippets of reviews that are used to generate answers about the features of a
|
||||
given place in Google Maps.
|
||||
"""
|
||||
|
||||
url: Optional[str] = None
|
||||
"""URI reference of the place."""
|
||||
|
||||
|
||||
class GoogleMapsResult(BaseModel):
|
||||
"""The result of the Google Maps."""
|
||||
|
||||
places: Optional[List[Place]] = None
|
||||
"""The places that were found."""
|
||||
|
||||
widget_context_token: Optional[str] = None
|
||||
"""Resource name of the Google Maps widget context token."""
|
||||
@@ -0,0 +1,39 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import List, Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
from .google_maps_result import GoogleMapsResult
|
||||
|
||||
__all__ = ["GoogleMapsResultContent"]
|
||||
|
||||
|
||||
class GoogleMapsResultContent(BaseModel):
|
||||
"""Google Maps result content."""
|
||||
|
||||
call_id: str
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: List[GoogleMapsResult]
|
||||
"""Required. The results of the Google Maps."""
|
||||
|
||||
type: Literal["google_maps_result"]
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
@@ -0,0 +1,46 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union, Iterable
|
||||
from typing_extensions import Literal, Required, Annotated, TypedDict
|
||||
|
||||
from .._types import Base64FileInput
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import set_pydantic_config
|
||||
from .google_maps_result_param import GoogleMapsResultParam
|
||||
|
||||
__all__ = ["GoogleMapsResultContentParam"]
|
||||
|
||||
|
||||
class GoogleMapsResultContentParam(TypedDict, total=False):
|
||||
"""Google Maps result content."""
|
||||
|
||||
call_id: Required[str]
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: Required[Iterable[GoogleMapsResultParam]]
|
||||
"""Required. The results of the Google Maps."""
|
||||
|
||||
type: Required[Literal["google_maps_result"]]
|
||||
|
||||
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
set_pydantic_config(GoogleMapsResultContentParam, {"arbitrary_types_allowed": True})
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
__all__ = ["GoogleMapsResultParam", "Place", "PlaceReviewSnippet"]
|
||||
|
||||
|
||||
class PlaceReviewSnippet(TypedDict, total=False):
|
||||
"""
|
||||
Encapsulates a snippet of a user review that answers a question about
|
||||
the features of a specific place in Google Maps.
|
||||
"""
|
||||
|
||||
review_id: str
|
||||
"""The ID of the review snippet."""
|
||||
|
||||
title: str
|
||||
"""Title of the review."""
|
||||
|
||||
url: str
|
||||
"""A link that corresponds to the user review on Google Maps."""
|
||||
|
||||
|
||||
class Place(TypedDict, total=False):
|
||||
name: str
|
||||
"""Title of the place."""
|
||||
|
||||
place_id: str
|
||||
"""The ID of the place, in `places/{place_id}` format."""
|
||||
|
||||
review_snippets: Iterable[PlaceReviewSnippet]
|
||||
"""
|
||||
Snippets of reviews that are used to generate answers about the features of a
|
||||
given place in Google Maps.
|
||||
"""
|
||||
|
||||
url: str
|
||||
"""URI reference of the place."""
|
||||
|
||||
|
||||
class GoogleMapsResultParam(TypedDict, total=False):
|
||||
"""The result of the Google Maps."""
|
||||
|
||||
places: Iterable[Place]
|
||||
"""The places that were found."""
|
||||
|
||||
widget_context_token: str
|
||||
"""Resource name of the Google Maps widget context token."""
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["GoogleSearchCallArguments"]
|
||||
|
||||
|
||||
class GoogleSearchCallArguments(BaseModel):
|
||||
"""The arguments to pass to Google Search."""
|
||||
|
||||
queries: Optional[List[str]] = None
|
||||
"""Web search queries for the following-up web search."""
|
||||
@@ -0,0 +1,31 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from .._types import SequenceNotStr
|
||||
|
||||
__all__ = ["GoogleSearchCallArgumentsParam"]
|
||||
|
||||
|
||||
class GoogleSearchCallArgumentsParam(TypedDict, total=False):
|
||||
"""The arguments to pass to Google Search."""
|
||||
|
||||
queries: SequenceNotStr[str]
|
||||
"""Web search queries for the following-up web search."""
|
||||
@@ -0,0 +1,42 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
from .google_search_call_arguments import GoogleSearchCallArguments
|
||||
|
||||
__all__ = ["GoogleSearchCallContent"]
|
||||
|
||||
|
||||
class GoogleSearchCallContent(BaseModel):
|
||||
"""Google Search content."""
|
||||
|
||||
id: str
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
arguments: GoogleSearchCallArguments
|
||||
"""Required. The arguments to pass to Google Search."""
|
||||
|
||||
type: Literal["google_search_call"]
|
||||
|
||||
search_type: Optional[Literal["web_search", "image_search", "enterprise_web_search"]] = None
|
||||
"""The type of search grounding enabled."""
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
@@ -0,0 +1,49 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
from typing_extensions import Literal, Required, Annotated, TypedDict
|
||||
|
||||
from .._types import Base64FileInput
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import set_pydantic_config
|
||||
from .google_search_call_arguments_param import GoogleSearchCallArgumentsParam
|
||||
|
||||
__all__ = ["GoogleSearchCallContentParam"]
|
||||
|
||||
|
||||
class GoogleSearchCallContentParam(TypedDict, total=False):
|
||||
"""Google Search content."""
|
||||
|
||||
id: Required[str]
|
||||
"""Required. A unique ID for this specific tool call."""
|
||||
|
||||
arguments: Required[GoogleSearchCallArgumentsParam]
|
||||
"""Required. The arguments to pass to Google Search."""
|
||||
|
||||
type: Required[Literal["google_search_call"]]
|
||||
|
||||
search_type: Literal["web_search", "image_search", "enterprise_web_search"]
|
||||
"""The type of search grounding enabled."""
|
||||
|
||||
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
set_pydantic_config(GoogleSearchCallContentParam, {"arbitrary_types_allowed": True})
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["GoogleSearchResult"]
|
||||
|
||||
|
||||
class GoogleSearchResult(BaseModel):
|
||||
"""The result of the Google Search."""
|
||||
|
||||
search_suggestions: Optional[str] = None
|
||||
"""Web content snippet that can be embedded in a web page or an app webview."""
|
||||
@@ -0,0 +1,42 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import List, Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
from .google_search_result import GoogleSearchResult
|
||||
|
||||
__all__ = ["GoogleSearchResultContent"]
|
||||
|
||||
|
||||
class GoogleSearchResultContent(BaseModel):
|
||||
"""Google Search result content."""
|
||||
|
||||
call_id: str
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: List[GoogleSearchResult]
|
||||
"""Required. The results of the Google Search."""
|
||||
|
||||
type: Literal["google_search_result"]
|
||||
|
||||
is_error: Optional[bool] = None
|
||||
"""Whether the Google Search resulted in an error."""
|
||||
|
||||
signature: Optional[str] = None
|
||||
"""A signature hash for backend validation."""
|
||||
@@ -0,0 +1,49 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union, Iterable
|
||||
from typing_extensions import Literal, Required, Annotated, TypedDict
|
||||
|
||||
from .._types import Base64FileInput
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import set_pydantic_config
|
||||
from .google_search_result_param import GoogleSearchResultParam
|
||||
|
||||
__all__ = ["GoogleSearchResultContentParam"]
|
||||
|
||||
|
||||
class GoogleSearchResultContentParam(TypedDict, total=False):
|
||||
"""Google Search result content."""
|
||||
|
||||
call_id: Required[str]
|
||||
"""Required. ID to match the ID from the function call block."""
|
||||
|
||||
result: Required[Iterable[GoogleSearchResultParam]]
|
||||
"""Required. The results of the Google Search."""
|
||||
|
||||
type: Required[Literal["google_search_result"]]
|
||||
|
||||
is_error: bool
|
||||
"""Whether the Google Search resulted in an error."""
|
||||
|
||||
signature: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
|
||||
"""A signature hash for backend validation."""
|
||||
|
||||
|
||||
set_pydantic_config(GoogleSearchResultContentParam, {"arbitrary_types_allowed": True})
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
__all__ = ["GoogleSearchResultParam"]
|
||||
|
||||
|
||||
class GoogleSearchResultParam(TypedDict, total=False):
|
||||
"""The result of the Google Search."""
|
||||
|
||||
search_suggestions: str
|
||||
"""Web content snippet that can be embedded in a web page or an app webview."""
|
||||
@@ -0,0 +1,33 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["ImageConfig"]
|
||||
|
||||
|
||||
class ImageConfig(BaseModel):
|
||||
"""The configuration for image interaction."""
|
||||
|
||||
aspect_ratio: Optional[
|
||||
Literal["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:8", "8:1", "1:4", "4:1"]
|
||||
] = None
|
||||
|
||||
image_size: Optional[Literal["1K", "2K", "4K", "512"]] = None
|
||||
@@ -0,0 +1,32 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing_extensions import Literal, TypedDict
|
||||
|
||||
__all__ = ["ImageConfigParam"]
|
||||
|
||||
|
||||
class ImageConfigParam(TypedDict, total=False):
|
||||
"""The configuration for image interaction."""
|
||||
|
||||
aspect_ratio: Literal[
|
||||
"1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:8", "8:1", "1:4", "4:1"
|
||||
]
|
||||
|
||||
image_size: Literal["1K", "2K", "4K", "512"]
|
||||
@@ -0,0 +1,45 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from typing import Optional
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .._models import BaseModel
|
||||
|
||||
__all__ = ["ImageContent"]
|
||||
|
||||
|
||||
class ImageContent(BaseModel):
|
||||
"""An image content block."""
|
||||
|
||||
type: Literal["image"]
|
||||
|
||||
data: Optional[str] = None
|
||||
"""The image content."""
|
||||
|
||||
mime_type: Optional[
|
||||
Literal[
|
||||
"image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff"
|
||||
]
|
||||
] = None
|
||||
"""The mime type of the image."""
|
||||
|
||||
resolution: Optional[Literal["low", "medium", "high", "ultra_high"]] = None
|
||||
"""The resolution of the media."""
|
||||
|
||||
uri: Optional[str] = None
|
||||
"""The URI of the image."""
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
from typing_extensions import Literal, Required, Annotated, TypedDict
|
||||
|
||||
from .._types import Base64FileInput
|
||||
from .._utils import PropertyInfo
|
||||
from .._models import set_pydantic_config
|
||||
|
||||
__all__ = ["ImageContentParam"]
|
||||
|
||||
|
||||
class ImageContentParam(TypedDict, total=False):
|
||||
"""An image content block."""
|
||||
|
||||
type: Required[Literal["image"]]
|
||||
|
||||
data: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")]
|
||||
"""The image content."""
|
||||
|
||||
mime_type: Literal[
|
||||
"image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff"
|
||||
]
|
||||
"""The mime type of the image."""
|
||||
|
||||
resolution: Literal["low", "medium", "high", "ultra_high"]
|
||||
"""The resolution of the media."""
|
||||
|
||||
uri: str
|
||||
"""The URI of the image."""
|
||||
|
||||
|
||||
set_pydantic_config(ImageContentParam, {"arbitrary_types_allowed": True})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user