google-genai 1.61.0__py3-none-any.whl → 1.62.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -101,6 +101,7 @@ from ._exceptions import (
101
101
  APIConnectionError,
102
102
  APIResponseValidationError,
103
103
  )
104
+ from ._utils._json import openapi_dumps
104
105
 
105
106
  log: logging.Logger = logging.getLogger(__name__)
106
107
 
@@ -578,8 +579,10 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
578
579
  kwargs["content"] = options.content
579
580
  elif isinstance(json_data, bytes):
580
581
  kwargs["content"] = json_data
581
- else:
582
- kwargs["json"] = json_data if is_given(json_data) else None
582
+ elif not files:
583
+ # Don't set content when JSON is sent as multipart/form-data,
584
+ # since httpx's content param overrides other body arguments
585
+ kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None
583
586
  kwargs["files"] = files
584
587
  else:
585
588
  headers.pop("Content-Type", None)
@@ -154,6 +154,7 @@ def model_dump(
154
154
  exclude_defaults: bool = False,
155
155
  warnings: bool = True,
156
156
  mode: Literal["json", "python"] = "python",
157
+ by_alias: bool | None = None,
157
158
  ) -> dict[str, Any]:
158
159
  if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
159
160
  return model.model_dump(
@@ -163,13 +164,12 @@ def model_dump(
163
164
  exclude_defaults=exclude_defaults,
164
165
  # warnings are not supported in Pydantic v1
165
166
  warnings=True if PYDANTIC_V1 else warnings,
167
+ by_alias=by_alias,
166
168
  )
167
169
  return cast(
168
170
  "dict[str, Any]",
169
171
  model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
170
- exclude=exclude,
171
- exclude_unset=exclude_unset,
172
- exclude_defaults=exclude_defaults,
172
+ exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias)
173
173
  ),
174
174
  )
175
175
 
@@ -0,0 +1,50 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+
16
+ import json
17
+ from typing import Any
18
+ from datetime import datetime
19
+ from typing_extensions import override
20
+
21
+ import pydantic
22
+
23
+ from .._compat import model_dump
24
+
25
+
26
+ def openapi_dumps(obj: Any) -> bytes:
27
+ """
28
+ Serialize an object to UTF-8 encoded JSON bytes.
29
+
30
+ Extends the standard json.dumps with support for additional types
31
+ commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc.
32
+ """
33
+ return json.dumps(
34
+ obj,
35
+ cls=_CustomEncoder,
36
+ # Uses the same defaults as httpx's JSON serialization
37
+ ensure_ascii=False,
38
+ separators=(",", ":"),
39
+ allow_nan=False,
40
+ ).encode()
41
+
42
+
43
+ class _CustomEncoder(json.JSONEncoder):
44
+ @override
45
+ def default(self, o: Any) -> Any:
46
+ if isinstance(o, datetime):
47
+ return o.isoformat()
48
+ if isinstance(o, pydantic.BaseModel):
49
+ return model_dump(o, exclude_unset=True, mode="json", by_alias=True)
50
+ return super().default(o)
google/genai/errors.py CHANGED
@@ -18,6 +18,7 @@
18
18
  from typing import Any, Callable, Optional, TYPE_CHECKING, Union
19
19
  import httpx
20
20
  import json
21
+ import websockets
21
22
  from . import _common
22
23
 
23
24
 
@@ -69,14 +70,26 @@ class APIError(Exception):
69
70
  return obj
70
71
 
71
72
  def _get_status(self, response_json: Any) -> Any:
72
- return response_json.get(
73
- 'status', response_json.get('error', {}).get('status', None)
74
- )
73
+ try:
74
+ status = response_json.get(
75
+ 'status', response_json.get('error', {}).get('status', None)
76
+ )
77
+ return status
78
+ except AttributeError:
79
+ # If response_json is not a dict, return close code to handle the case
80
+ # when encountering a websocket error.
81
+ return None
75
82
 
76
83
  def _get_message(self, response_json: Any) -> Any:
77
- return response_json.get(
78
- 'message', response_json.get('error', {}).get('message', None)
79
- )
84
+ try:
85
+ message = response_json.get(
86
+ 'message', response_json.get('error', {}).get('message', None)
87
+ )
88
+ return message
89
+ except AttributeError:
90
+ # If response_json is not a dict, return it as None.
91
+ # This is to handle the case when encountering a websocket error.
92
+ return None
80
93
 
81
94
  def _get_code(self, response_json: Any) -> Any:
82
95
  return response_json.get(
google/genai/live.py CHANGED
@@ -26,7 +26,7 @@ import warnings
26
26
 
27
27
  import google.auth
28
28
  import pydantic
29
- from websockets import ConnectionClosed
29
+ import websockets
30
30
 
31
31
  from . import _api_module
32
32
  from . import _common
@@ -41,6 +41,7 @@ from ._common import set_value_by_path as setv
41
41
  from .live_music import AsyncLiveMusic
42
42
  from .models import _Content_to_mldev
43
43
 
44
+ ConnectionClosed = websockets.ConnectionClosed
44
45
 
45
46
  try:
46
47
  from websockets.asyncio.client import ClientConnection
@@ -534,6 +535,14 @@ class AsyncSession:
534
535
  raw_response = await self._ws.recv(decode=False)
535
536
  except TypeError:
536
537
  raw_response = await self._ws.recv() # type: ignore[assignment]
538
+ except ConnectionClosed as e:
539
+ if e.rcvd:
540
+ code = e.rcvd.code
541
+ reason = e.rcvd.reason
542
+ else:
543
+ code = 1006
544
+ reason = websockets.frames.CLOSE_CODE_EXPLANATIONS.get(code, 'Abnormal closure.')
545
+ errors.APIError.raise_error(code, reason, None)
537
546
  if raw_response:
538
547
  try:
539
548
  response = json.loads(raw_response)
@@ -545,8 +554,11 @@ class AsyncSession:
545
554
  if self._api_client.vertexai:
546
555
  response_dict = live_converters._LiveServerMessage_from_vertex(response)
547
556
  else:
548
- response_dict = response
557
+ response_dict = live_converters._LiveServerMessage_from_mldev(response)
549
558
 
559
+ if not response_dict and response:
560
+ # Error handling.
561
+ errors.APIError.raise_error(response.get('code'), response, None)
550
562
  return types.LiveServerMessage._from_response(
551
563
  response=response_dict, kwargs=parameter_model.model_dump()
552
564
  )
@@ -1093,6 +1105,14 @@ class AsyncLive(_api_module.BaseModule):
1093
1105
  raw_response = await ws.recv(decode=False)
1094
1106
  except TypeError:
1095
1107
  raw_response = await ws.recv() # type: ignore[assignment]
1108
+ except ConnectionClosed as e:
1109
+ if e.rcvd:
1110
+ code = e.rcvd.code
1111
+ reason = e.rcvd.reason
1112
+ else:
1113
+ code = 1006
1114
+ reason = 'Abnormal closure.'
1115
+ errors.APIError.raise_error(code, reason, None)
1096
1116
  if raw_response:
1097
1117
  try:
1098
1118
  response = json.loads(raw_response)
@@ -19,15 +19,18 @@ import contextlib
19
19
  import json
20
20
  import logging
21
21
  from typing import AsyncIterator
22
+ import websockets
22
23
 
23
24
  from . import _api_module
24
25
  from . import _common
25
26
  from . import _live_converters as live_converters
26
27
  from . import _transformers as t
28
+ from . import errors
27
29
  from . import types
28
30
  from ._api_client import BaseApiClient
29
31
  from ._common import set_value_by_path as setv
30
32
 
33
+ ConnectionClosed = websockets.ConnectionClosed
31
34
 
32
35
  try:
33
36
  from websockets.asyncio.client import ClientConnection
@@ -122,6 +125,14 @@ class AsyncMusicSession:
122
125
  raw_response = await self._ws.recv(decode=False)
123
126
  except TypeError:
124
127
  raw_response = await self._ws.recv() # type: ignore[assignment]
128
+ except ConnectionClosed as e:
129
+ if e.rcvd:
130
+ code = e.rcvd.code
131
+ reason = e.rcvd.reason
132
+ else:
133
+ code = 1006
134
+ reason = websockets.frames.CLOSE_CODE_EXPLANATIONS.get(code, 'Abnormal closure.')
135
+ errors.APIError.raise_error(code, reason, None)
125
136
  if raw_response:
126
137
  try:
127
138
  response = json.loads(raw_response)
@@ -134,7 +145,9 @@ class AsyncMusicSession:
134
145
  raise NotImplementedError('Live music generation is not supported in Vertex AI.')
135
146
  else:
136
147
  response_dict = response
137
-
148
+ if not response_dict and response:
149
+ # Error handling.
150
+ errors.APIError.raise_error(response.get('code'), response, None)
138
151
  return types.LiveMusicServerMessage._from_response(
139
152
  response=response_dict, kwargs=parameter_model.model_dump()
140
153
  )