google-genai 1.7.0__py3-none-any.whl → 1.53.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- google/genai/__init__.py +4 -2
- google/genai/_adapters.py +55 -0
- google/genai/_api_client.py +1301 -299
- google/genai/_api_module.py +1 -1
- google/genai/_automatic_function_calling_util.py +54 -33
- google/genai/_base_transformers.py +26 -0
- google/genai/_base_url.py +50 -0
- google/genai/_common.py +560 -59
- google/genai/_extra_utils.py +371 -38
- google/genai/_live_converters.py +1467 -0
- google/genai/_local_tokenizer_loader.py +214 -0
- google/genai/_mcp_utils.py +117 -0
- google/genai/_operations_converters.py +394 -0
- google/genai/_replay_api_client.py +204 -92
- google/genai/_test_api_client.py +1 -1
- google/genai/_tokens_converters.py +520 -0
- google/genai/_transformers.py +633 -233
- google/genai/batches.py +1733 -538
- google/genai/caches.py +678 -1012
- google/genai/chats.py +48 -38
- google/genai/client.py +142 -15
- google/genai/documents.py +532 -0
- google/genai/errors.py +141 -35
- google/genai/file_search_stores.py +1296 -0
- google/genai/files.py +312 -744
- google/genai/live.py +617 -367
- google/genai/live_music.py +197 -0
- google/genai/local_tokenizer.py +395 -0
- google/genai/models.py +3598 -3116
- google/genai/operations.py +201 -362
- google/genai/pagers.py +23 -7
- google/genai/py.typed +1 -0
- google/genai/tokens.py +362 -0
- google/genai/tunings.py +1274 -496
- google/genai/types.py +14535 -5454
- google/genai/version.py +2 -2
- {google_genai-1.7.0.dist-info → google_genai-1.53.0.dist-info}/METADATA +736 -234
- google_genai-1.53.0.dist-info/RECORD +41 -0
- {google_genai-1.7.0.dist-info → google_genai-1.53.0.dist-info}/WHEEL +1 -1
- google_genai-1.7.0.dist-info/RECORD +0 -27
- {google_genai-1.7.0.dist-info → google_genai-1.53.0.dist-info/licenses}/LICENSE +0 -0
- {google_genai-1.7.0.dist-info → google_genai-1.53.0.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright
|
|
1
|
+
# Copyright 2025 Google LLC
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -17,28 +17,82 @@
|
|
|
17
17
|
|
|
18
18
|
import base64
|
|
19
19
|
import copy
|
|
20
|
-
import
|
|
20
|
+
import contextlib
|
|
21
|
+
import enum
|
|
21
22
|
import inspect
|
|
22
23
|
import io
|
|
23
24
|
import json
|
|
24
25
|
import os
|
|
25
26
|
import re
|
|
26
|
-
from typing import Any, Literal, Optional, Union
|
|
27
|
+
from typing import Any, Literal, Optional, Union, Iterator, AsyncIterator
|
|
27
28
|
|
|
28
29
|
import google.auth
|
|
29
|
-
from requests.exceptions import HTTPError
|
|
30
30
|
|
|
31
31
|
from . import errors
|
|
32
32
|
from ._api_client import BaseApiClient
|
|
33
|
-
from ._api_client import HttpOptions
|
|
34
33
|
from ._api_client import HttpRequest
|
|
35
34
|
from ._api_client import HttpResponse
|
|
36
35
|
from ._common import BaseModel
|
|
36
|
+
from .types import HttpOptions, HttpOptionsOrDict
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def to_snake_case(name: str) -> str:
|
|
40
|
+
"""Converts a string from camelCase or PascalCase to snake_case."""
|
|
41
|
+
|
|
42
|
+
if not isinstance(name, str):
|
|
43
|
+
name = str(name)
|
|
44
|
+
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
|
45
|
+
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _normalize_json_case(obj: Any) -> Any:
|
|
49
|
+
if isinstance(obj, dict):
|
|
50
|
+
return {
|
|
51
|
+
to_snake_case(k): _normalize_json_case(v)
|
|
52
|
+
for k, v in obj.items()
|
|
53
|
+
}
|
|
54
|
+
elif isinstance(obj, list):
|
|
55
|
+
return [_normalize_json_case(item) for item in obj]
|
|
56
|
+
elif isinstance(obj, enum.Enum):
|
|
57
|
+
return obj.value
|
|
58
|
+
elif isinstance(obj, str):
|
|
59
|
+
# Python >= 3.14 has a new division by zero error message.
|
|
60
|
+
if 'division by zero' in obj:
|
|
61
|
+
return obj.replace(
|
|
62
|
+
'division by zero', 'integer division or modulo by zero'
|
|
63
|
+
)
|
|
64
|
+
return obj
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _equals_ignore_key_case(obj1: Any, obj2: Any) -> bool:
|
|
68
|
+
"""Compares two Python objects for equality ignoring key casing.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
bool: True if the two objects are equal regardless of key casing
|
|
72
|
+
(camelCase vs. snake_case). For example, the following are considered equal:
|
|
73
|
+
|
|
74
|
+
{'my_key': 'my_value'}
|
|
75
|
+
{'myKey': 'my_value'}
|
|
76
|
+
|
|
77
|
+
This also considers enums and strings with the same value as equal.
|
|
78
|
+
For example, the following are considered equal:
|
|
79
|
+
|
|
80
|
+
{'type': <Type.STRING: 'STRING'>}}
|
|
81
|
+
{'type': 'STRING'}
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
normalized_obj_1 = _normalize_json_case(obj1)
|
|
85
|
+
normalized_obj_2 = _normalize_json_case(obj2)
|
|
86
|
+
|
|
87
|
+
if normalized_obj_1 == normalized_obj_2:
|
|
88
|
+
return True
|
|
89
|
+
else:
|
|
90
|
+
return False
|
|
37
91
|
|
|
38
92
|
|
|
39
93
|
def _redact_version_numbers(version_string: str) -> str:
|
|
40
94
|
"""Redacts version numbers in the form x.y.z from a string."""
|
|
41
|
-
return re.sub(r'\d+\.\d+\.\d+', '{VERSION_NUMBER}', version_string)
|
|
95
|
+
return re.sub(r'\d+\.\d+\.\d+[a-zA-Z0-9]*', '{VERSION_NUMBER}', version_string)
|
|
42
96
|
|
|
43
97
|
|
|
44
98
|
def _redact_language_label(language_label: str) -> str:
|
|
@@ -46,7 +100,7 @@ def _redact_language_label(language_label: str) -> str:
|
|
|
46
100
|
return re.sub(r'gl-python/', '{LANGUAGE_LABEL}/', language_label)
|
|
47
101
|
|
|
48
102
|
|
|
49
|
-
def _redact_request_headers(headers):
|
|
103
|
+
def _redact_request_headers(headers: dict[str, str]) -> dict[str, str]:
|
|
50
104
|
"""Redacts headers that should not be recorded."""
|
|
51
105
|
redacted_headers = {}
|
|
52
106
|
for header_name, header_value in headers.items():
|
|
@@ -88,7 +142,7 @@ def _redact_request_url(url: str) -> str:
|
|
|
88
142
|
result,
|
|
89
143
|
)
|
|
90
144
|
result = re.sub(
|
|
91
|
-
r'
|
|
145
|
+
r'.*generativelanguage.*.googleapis.com/[^/]+',
|
|
92
146
|
'{MLDEV_URL_PREFIX}',
|
|
93
147
|
result,
|
|
94
148
|
)
|
|
@@ -109,28 +163,36 @@ def _redact_project_location_path(path: str) -> str:
|
|
|
109
163
|
return path
|
|
110
164
|
|
|
111
165
|
|
|
112
|
-
def _redact_request_body(body: dict[str, object]):
|
|
166
|
+
def _redact_request_body(body: dict[str, object]) -> None:
|
|
113
167
|
"""Redacts fields in the request body in place."""
|
|
114
168
|
for key, value in body.items():
|
|
115
169
|
if isinstance(value, str):
|
|
116
170
|
body[key] = _redact_project_location_path(value)
|
|
117
171
|
|
|
118
172
|
|
|
119
|
-
def redact_http_request(http_request: HttpRequest):
|
|
173
|
+
def redact_http_request(http_request: HttpRequest) -> None:
|
|
120
174
|
http_request.headers = _redact_request_headers(http_request.headers)
|
|
121
175
|
http_request.url = _redact_request_url(http_request.url)
|
|
122
|
-
|
|
176
|
+
if not isinstance(http_request.data, bytes):
|
|
177
|
+
_redact_request_body(http_request.data)
|
|
123
178
|
|
|
124
179
|
|
|
125
|
-
def _current_file_path_and_line():
|
|
180
|
+
def _current_file_path_and_line() -> str:
|
|
126
181
|
"""Prints the current file path and line number."""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
182
|
+
current_frame = inspect.currentframe()
|
|
183
|
+
if (
|
|
184
|
+
current_frame is not None
|
|
185
|
+
and current_frame.f_back is not None
|
|
186
|
+
and current_frame.f_back.f_back is not None
|
|
187
|
+
):
|
|
188
|
+
frame = current_frame.f_back.f_back
|
|
189
|
+
filepath = inspect.getfile(frame)
|
|
190
|
+
lineno = frame.f_lineno
|
|
191
|
+
return f'File: {filepath}, Line: {lineno}'
|
|
192
|
+
return ''
|
|
131
193
|
|
|
132
194
|
|
|
133
|
-
def _debug_print(message: str):
|
|
195
|
+
def _debug_print(message: str) -> None:
|
|
134
196
|
print(
|
|
135
197
|
'DEBUG (test',
|
|
136
198
|
os.environ.get('PYTEST_CURRENT_TEST'),
|
|
@@ -141,6 +203,28 @@ def _debug_print(message: str):
|
|
|
141
203
|
)
|
|
142
204
|
|
|
143
205
|
|
|
206
|
+
def pop_undeterministic_headers(headers: dict[str, str]) -> None:
|
|
207
|
+
"""Remove headers that are not deterministic."""
|
|
208
|
+
headers.pop('Date', None) # pytype: disable=attribute-error
|
|
209
|
+
headers.pop('Server-Timing', None) # pytype: disable=attribute-error
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@contextlib.contextmanager
|
|
213
|
+
def _record_on_api_error(client: 'ReplayApiClient', http_request: HttpRequest) -> Iterator[None]:
|
|
214
|
+
try:
|
|
215
|
+
yield
|
|
216
|
+
except errors.APIError as e:
|
|
217
|
+
client._record_interaction(http_request, e)
|
|
218
|
+
raise e
|
|
219
|
+
|
|
220
|
+
@contextlib.asynccontextmanager
|
|
221
|
+
async def _async_record_on_api_error(client: 'ReplayApiClient', http_request: HttpRequest) -> AsyncIterator[None]:
|
|
222
|
+
try:
|
|
223
|
+
yield
|
|
224
|
+
except errors.APIError as e:
|
|
225
|
+
client._record_interaction(http_request, e)
|
|
226
|
+
raise e
|
|
227
|
+
|
|
144
228
|
class ReplayRequest(BaseModel):
|
|
145
229
|
"""Represents a single request in a replay."""
|
|
146
230
|
|
|
@@ -160,10 +244,7 @@ class ReplayResponse(BaseModel):
|
|
|
160
244
|
sdk_response_segments: list[dict[str, object]]
|
|
161
245
|
|
|
162
246
|
def model_post_init(self, __context: Any) -> None:
|
|
163
|
-
|
|
164
|
-
# every time they are recorded.
|
|
165
|
-
self.headers.pop('Date', None)
|
|
166
|
-
self.headers.pop('Server-Timing', None)
|
|
247
|
+
pop_undeterministic_headers(self.headers)
|
|
167
248
|
|
|
168
249
|
|
|
169
250
|
class ReplayInteraction(BaseModel):
|
|
@@ -194,6 +275,7 @@ class ReplayApiClient(BaseApiClient):
|
|
|
194
275
|
project: Optional[str] = None,
|
|
195
276
|
location: Optional[str] = None,
|
|
196
277
|
http_options: Optional[HttpOptions] = None,
|
|
278
|
+
private: bool = False,
|
|
197
279
|
):
|
|
198
280
|
super().__init__(
|
|
199
281
|
vertexai=vertexai,
|
|
@@ -209,33 +291,34 @@ class ReplayApiClient(BaseApiClient):
|
|
|
209
291
|
'GOOGLE_GENAI_REPLAYS_DIRECTORY', None
|
|
210
292
|
)
|
|
211
293
|
# Valid replay modes are replay-only or record-and-replay.
|
|
212
|
-
self.replay_session = None
|
|
294
|
+
self.replay_session: Union[ReplayFile, None] = None
|
|
213
295
|
self._mode = mode
|
|
214
296
|
self._replay_id = replay_id
|
|
297
|
+
self._private = private
|
|
215
298
|
|
|
216
|
-
def initialize_replay_session(self, replay_id: str):
|
|
299
|
+
def initialize_replay_session(self, replay_id: str) -> None:
|
|
217
300
|
self._replay_id = replay_id
|
|
218
301
|
self._initialize_replay_session()
|
|
219
302
|
|
|
220
|
-
def _get_replay_file_path(self):
|
|
303
|
+
def _get_replay_file_path(self) -> str:
|
|
221
304
|
return self._generate_file_path_from_replay_id(
|
|
222
305
|
self.replays_directory, self._replay_id
|
|
223
306
|
)
|
|
224
307
|
|
|
225
|
-
def _should_call_api(self):
|
|
308
|
+
def _should_call_api(self) -> bool:
|
|
226
309
|
return self._mode in ['record', 'api'] or (
|
|
227
310
|
self._mode == 'auto'
|
|
228
311
|
and not os.path.isfile(self._get_replay_file_path())
|
|
229
312
|
)
|
|
230
313
|
|
|
231
|
-
def _should_update_replay(self):
|
|
314
|
+
def _should_update_replay(self) -> bool:
|
|
232
315
|
return self._should_call_api() and self._mode != 'api'
|
|
233
316
|
|
|
234
|
-
def _initialize_replay_session_if_not_loaded(self):
|
|
317
|
+
def _initialize_replay_session_if_not_loaded(self) -> None:
|
|
235
318
|
if not self.replay_session:
|
|
236
319
|
self._initialize_replay_session()
|
|
237
320
|
|
|
238
|
-
def _initialize_replay_session(self):
|
|
321
|
+
def _initialize_replay_session(self) -> None:
|
|
239
322
|
_debug_print('Test is using replay id: ' + self._replay_id)
|
|
240
323
|
self._replay_index = 0
|
|
241
324
|
self._sdk_response_index = 0
|
|
@@ -256,7 +339,7 @@ class ReplayApiClient(BaseApiClient):
|
|
|
256
339
|
replay_id=self._replay_id, interactions=[]
|
|
257
340
|
)
|
|
258
341
|
|
|
259
|
-
def _generate_file_path_from_replay_id(self, replay_directory, replay_id):
|
|
342
|
+
def _generate_file_path_from_replay_id(self, replay_directory: Optional[str], replay_id: str) -> str:
|
|
260
343
|
session_parts = replay_id.split('/')
|
|
261
344
|
if len(session_parts) < 3:
|
|
262
345
|
raise ValueError(
|
|
@@ -270,7 +353,7 @@ class ReplayApiClient(BaseApiClient):
|
|
|
270
353
|
path_parts.extend(session_parts)
|
|
271
354
|
return os.path.join(*path_parts) + '.json'
|
|
272
355
|
|
|
273
|
-
def close(self):
|
|
356
|
+
def close(self) -> None:
|
|
274
357
|
if not self._should_update_replay() or not self.replay_session:
|
|
275
358
|
return
|
|
276
359
|
replay_file_path = self._get_replay_file_path()
|
|
@@ -283,7 +366,7 @@ class ReplayApiClient(BaseApiClient):
|
|
|
283
366
|
self,
|
|
284
367
|
http_request: HttpRequest,
|
|
285
368
|
http_response: Union[HttpResponse, errors.APIError, bytes],
|
|
286
|
-
):
|
|
369
|
+
) -> None:
|
|
287
370
|
if not self._should_update_replay():
|
|
288
371
|
return
|
|
289
372
|
redact_http_request(http_request)
|
|
@@ -321,6 +404,8 @@ class ReplayApiClient(BaseApiClient):
|
|
|
321
404
|
raise ValueError(
|
|
322
405
|
'Unsupported http_response type: ' + str(type(http_response))
|
|
323
406
|
)
|
|
407
|
+
if self.replay_session is None:
|
|
408
|
+
raise ValueError('No replay session found.')
|
|
324
409
|
self.replay_session.interactions.append(
|
|
325
410
|
ReplayInteraction(request=request, response=response)
|
|
326
411
|
)
|
|
@@ -329,7 +414,9 @@ class ReplayApiClient(BaseApiClient):
|
|
|
329
414
|
self,
|
|
330
415
|
http_request: HttpRequest,
|
|
331
416
|
interaction: ReplayInteraction,
|
|
332
|
-
):
|
|
417
|
+
) -> None:
|
|
418
|
+
_debug_print(f'http_request.url: {http_request.url}')
|
|
419
|
+
_debug_print(f'interaction.request.url: {interaction.request.url}')
|
|
333
420
|
assert http_request.url == interaction.request.url
|
|
334
421
|
assert http_request.headers == interaction.request.headers, (
|
|
335
422
|
'Request headers mismatch:\n'
|
|
@@ -342,26 +429,29 @@ class ReplayApiClient(BaseApiClient):
|
|
|
342
429
|
request_data_copy = copy.deepcopy(http_request.data)
|
|
343
430
|
# Both the request and recorded request must be redacted before comparing
|
|
344
431
|
# so that the comparison is fair.
|
|
345
|
-
|
|
432
|
+
if not isinstance(request_data_copy, bytes):
|
|
433
|
+
_redact_request_body(request_data_copy)
|
|
346
434
|
|
|
347
435
|
actual_request_body = [request_data_copy]
|
|
348
436
|
expected_request_body = interaction.request.body_segments
|
|
349
|
-
assert actual_request_body
|
|
437
|
+
assert _equals_ignore_key_case(actual_request_body, expected_request_body), (
|
|
350
438
|
'Request body mismatch:\n'
|
|
351
439
|
f'Actual: {actual_request_body}\n'
|
|
352
440
|
f'Expected: {expected_request_body}'
|
|
353
441
|
)
|
|
354
442
|
|
|
355
|
-
def _build_response_from_replay(self, http_request: HttpRequest):
|
|
443
|
+
def _build_response_from_replay(self, http_request: HttpRequest) -> HttpResponse:
|
|
356
444
|
redact_http_request(http_request)
|
|
357
445
|
|
|
446
|
+
if self.replay_session is None:
|
|
447
|
+
raise ValueError('No replay session found.')
|
|
358
448
|
interaction = self.replay_session.interactions[self._replay_index]
|
|
359
449
|
# Replay is on the right side of the assert so the diff makes more sense.
|
|
360
450
|
self._match_request(http_request, interaction)
|
|
361
451
|
self._replay_index += 1
|
|
362
452
|
self._sdk_response_index = 0
|
|
363
453
|
errors.APIError.raise_for_response(interaction.response)
|
|
364
|
-
|
|
454
|
+
http_response = HttpResponse(
|
|
365
455
|
headers=interaction.response.headers,
|
|
366
456
|
response_stream=[
|
|
367
457
|
json.dumps(segment)
|
|
@@ -369,17 +459,29 @@ class ReplayApiClient(BaseApiClient):
|
|
|
369
459
|
],
|
|
370
460
|
byte_stream=interaction.response.byte_segments,
|
|
371
461
|
)
|
|
462
|
+
if http_response.response_stream == ['{}']:
|
|
463
|
+
http_response.response_stream = [""]
|
|
464
|
+
return http_response
|
|
372
465
|
|
|
373
|
-
def _verify_response(self, response_model: BaseModel):
|
|
466
|
+
def _verify_response(self, response_model: BaseModel) -> None:
|
|
374
467
|
if self._mode == 'api':
|
|
375
468
|
return
|
|
469
|
+
if not self.replay_session:
|
|
470
|
+
raise ValueError('No replay session found.')
|
|
376
471
|
# replay_index is advanced in _build_response_from_replay, so we need to -1.
|
|
377
472
|
interaction = self.replay_session.interactions[self._replay_index - 1]
|
|
378
473
|
if self._should_update_replay():
|
|
379
474
|
if isinstance(response_model, list):
|
|
380
475
|
response_model = response_model[0]
|
|
381
|
-
|
|
382
|
-
|
|
476
|
+
sdk_response_response = getattr(response_model, 'sdk_http_response', None)
|
|
477
|
+
if response_model and (
|
|
478
|
+
sdk_response_response is not None
|
|
479
|
+
):
|
|
480
|
+
headers = getattr(
|
|
481
|
+
sdk_response_response, 'headers', None
|
|
482
|
+
)
|
|
483
|
+
if headers:
|
|
484
|
+
pop_undeterministic_headers(headers)
|
|
383
485
|
interaction.response.sdk_response_segments.append(
|
|
384
486
|
response_model.model_dump(exclude_none=True)
|
|
385
487
|
)
|
|
@@ -387,29 +489,46 @@ class ReplayApiClient(BaseApiClient):
|
|
|
387
489
|
|
|
388
490
|
if isinstance(response_model, list):
|
|
389
491
|
response_model = response_model[0]
|
|
390
|
-
|
|
492
|
+
_debug_print(
|
|
493
|
+
f'response_model: {response_model.model_dump(exclude_none=True)}'
|
|
494
|
+
)
|
|
391
495
|
actual = response_model.model_dump(exclude_none=True, mode='json')
|
|
392
496
|
expected = interaction.response.sdk_response_segments[
|
|
393
497
|
self._sdk_response_index
|
|
394
498
|
]
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
499
|
+
# The sdk_http_response.body has format in the string, need to get rid of
|
|
500
|
+
# the format information before comparing.
|
|
501
|
+
if isinstance(expected, dict):
|
|
502
|
+
if 'sdk_http_response' in expected and isinstance(
|
|
503
|
+
expected['sdk_http_response'], dict
|
|
504
|
+
):
|
|
505
|
+
if 'body' in expected['sdk_http_response']:
|
|
506
|
+
raw_body = expected['sdk_http_response']['body']
|
|
507
|
+
_debug_print(f'raw_body length: {len(raw_body)}')
|
|
508
|
+
_debug_print(f'raw_body: {raw_body}')
|
|
509
|
+
if isinstance(raw_body, str) and raw_body != '':
|
|
510
|
+
raw_body = json.loads(raw_body)
|
|
511
|
+
raw_body = json.dumps(raw_body)
|
|
512
|
+
expected['sdk_http_response']['body'] = raw_body
|
|
513
|
+
if not self._private:
|
|
514
|
+
assert (
|
|
515
|
+
actual == expected
|
|
516
|
+
), f'SDK response mismatch:\nActual: {actual}\nExpected: {expected}'
|
|
517
|
+
else:
|
|
518
|
+
_debug_print(f'Expected SDK response mismatch:\nActual: {actual}\nExpected: {expected}')
|
|
398
519
|
self._sdk_response_index += 1
|
|
399
520
|
|
|
400
521
|
def _request(
|
|
401
522
|
self,
|
|
402
523
|
http_request: HttpRequest,
|
|
524
|
+
http_options: Optional[HttpOptionsOrDict] = None,
|
|
403
525
|
stream: bool = False,
|
|
404
526
|
) -> HttpResponse:
|
|
405
527
|
self._initialize_replay_session_if_not_loaded()
|
|
406
528
|
if self._should_call_api():
|
|
407
529
|
_debug_print('api mode request: %s' % http_request)
|
|
408
|
-
|
|
409
|
-
result = super()._request(http_request, stream)
|
|
410
|
-
except errors.APIError as e:
|
|
411
|
-
self._record_interaction(http_request, e)
|
|
412
|
-
raise e
|
|
530
|
+
with _record_on_api_error(self, http_request):
|
|
531
|
+
result = super()._request(http_request, http_options, stream)
|
|
413
532
|
if stream:
|
|
414
533
|
result_segments = []
|
|
415
534
|
for segment in result.segments():
|
|
@@ -428,16 +547,16 @@ class ReplayApiClient(BaseApiClient):
|
|
|
428
547
|
async def _async_request(
|
|
429
548
|
self,
|
|
430
549
|
http_request: HttpRequest,
|
|
550
|
+
http_options: Optional[HttpOptionsOrDict] = None,
|
|
431
551
|
stream: bool = False,
|
|
432
552
|
) -> HttpResponse:
|
|
433
553
|
self._initialize_replay_session_if_not_loaded()
|
|
434
554
|
if self._should_call_api():
|
|
435
555
|
_debug_print('api mode request: %s' % http_request)
|
|
436
|
-
|
|
437
|
-
result = await super()._async_request(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
raise e
|
|
556
|
+
async with _async_record_on_api_error(self, http_request):
|
|
557
|
+
result = await super()._async_request(
|
|
558
|
+
http_request, http_options, stream
|
|
559
|
+
)
|
|
441
560
|
if stream:
|
|
442
561
|
result_segments = []
|
|
443
562
|
async for segment in result.async_segments():
|
|
@@ -453,7 +572,14 @@ class ReplayApiClient(BaseApiClient):
|
|
|
453
572
|
else:
|
|
454
573
|
return self._build_response_from_replay(http_request)
|
|
455
574
|
|
|
456
|
-
def upload_file(
|
|
575
|
+
def upload_file(
|
|
576
|
+
self,
|
|
577
|
+
file_path: Union[str, io.IOBase],
|
|
578
|
+
upload_url: str,
|
|
579
|
+
upload_size: int,
|
|
580
|
+
*,
|
|
581
|
+
http_options: Optional[HttpOptionsOrDict] = None,
|
|
582
|
+
) -> HttpResponse:
|
|
457
583
|
if isinstance(file_path, io.IOBase):
|
|
458
584
|
offset = file_path.tell()
|
|
459
585
|
content = file_path.read()
|
|
@@ -470,25 +596,23 @@ class ReplayApiClient(BaseApiClient):
|
|
|
470
596
|
)
|
|
471
597
|
if self._should_call_api():
|
|
472
598
|
result: Union[str, HttpResponse]
|
|
473
|
-
|
|
474
|
-
result = super().upload_file(
|
|
475
|
-
|
|
476
|
-
result = HttpResponse(
|
|
477
|
-
e.response.headers, [json.dumps({'reason': e.response.reason})]
|
|
599
|
+
with _record_on_api_error(self, request):
|
|
600
|
+
result = super().upload_file(
|
|
601
|
+
file_path, upload_url, upload_size, http_options=http_options
|
|
478
602
|
)
|
|
479
|
-
|
|
480
|
-
raise e
|
|
481
|
-
self._record_interaction(request, HttpResponse({}, [json.dumps(result)]))
|
|
603
|
+
self._record_interaction(request, result)
|
|
482
604
|
return result
|
|
483
605
|
else:
|
|
484
|
-
return self._build_response_from_replay(request)
|
|
606
|
+
return self._build_response_from_replay(request)
|
|
485
607
|
|
|
486
608
|
async def async_upload_file(
|
|
487
609
|
self,
|
|
488
610
|
file_path: Union[str, io.IOBase],
|
|
489
611
|
upload_url: str,
|
|
490
612
|
upload_size: int,
|
|
491
|
-
|
|
613
|
+
*,
|
|
614
|
+
http_options: Optional[HttpOptionsOrDict] = None,
|
|
615
|
+
) -> HttpResponse:
|
|
492
616
|
if isinstance(file_path, io.IOBase):
|
|
493
617
|
offset = file_path.tell()
|
|
494
618
|
content = file_path.read()
|
|
@@ -504,55 +628,43 @@ class ReplayApiClient(BaseApiClient):
|
|
|
504
628
|
method='POST', url='', data={'file_path': file_path}, headers={}
|
|
505
629
|
)
|
|
506
630
|
if self._should_call_api():
|
|
507
|
-
result:
|
|
508
|
-
|
|
631
|
+
result: HttpResponse
|
|
632
|
+
async with _async_record_on_api_error(self, request):
|
|
509
633
|
result = await super().async_upload_file(
|
|
510
|
-
file_path, upload_url, upload_size
|
|
511
|
-
)
|
|
512
|
-
except HTTPError as e:
|
|
513
|
-
result = HttpResponse(
|
|
514
|
-
e.response.headers, [json.dumps({'reason': e.response.reason})]
|
|
634
|
+
file_path, upload_url, upload_size, http_options=http_options
|
|
515
635
|
)
|
|
516
|
-
|
|
517
|
-
raise e
|
|
518
|
-
self._record_interaction(request, HttpResponse({}, [json.dumps(result)]))
|
|
636
|
+
self._record_interaction(request, result)
|
|
519
637
|
return result
|
|
520
638
|
else:
|
|
521
|
-
return self._build_response_from_replay(request)
|
|
639
|
+
return self._build_response_from_replay(request)
|
|
522
640
|
|
|
523
|
-
def download_file(
|
|
641
|
+
def download_file(
|
|
642
|
+
self, path: str, *, http_options: Optional[HttpOptionsOrDict] = None
|
|
643
|
+
) -> Union[HttpResponse, bytes, Any]:
|
|
524
644
|
self._initialize_replay_session_if_not_loaded()
|
|
525
645
|
request = self._build_request(
|
|
526
646
|
'get', path=path, request_dict={}, http_options=http_options
|
|
527
647
|
)
|
|
528
648
|
if self._should_call_api():
|
|
529
|
-
|
|
530
|
-
result = super().download_file(path, http_options)
|
|
531
|
-
except HTTPError as e:
|
|
532
|
-
result = HttpResponse(
|
|
533
|
-
e.response.headers, [json.dumps({'reason': e.response.reason})]
|
|
534
|
-
)
|
|
535
|
-
result.status_code = e.response.status_code
|
|
536
|
-
raise e
|
|
649
|
+
with _record_on_api_error(self, request):
|
|
650
|
+
result = super().download_file(path, http_options=http_options)
|
|
537
651
|
self._record_interaction(request, result)
|
|
538
652
|
return result
|
|
539
653
|
else:
|
|
540
654
|
return self._build_response_from_replay(request).byte_stream[0]
|
|
541
655
|
|
|
542
|
-
async def async_download_file(
|
|
656
|
+
async def async_download_file(
|
|
657
|
+
self, path: str, *, http_options: Optional[HttpOptionsOrDict] = None
|
|
658
|
+
) -> Any:
|
|
543
659
|
self._initialize_replay_session_if_not_loaded()
|
|
544
660
|
request = self._build_request(
|
|
545
661
|
'get', path=path, request_dict={}, http_options=http_options
|
|
546
662
|
)
|
|
547
663
|
if self._should_call_api():
|
|
548
|
-
|
|
549
|
-
result = await super().async_download_file(
|
|
550
|
-
|
|
551
|
-
result = HttpResponse(
|
|
552
|
-
e.response.headers, [json.dumps({'reason': e.response.reason})]
|
|
664
|
+
async with _async_record_on_api_error(self, request):
|
|
665
|
+
result = await super().async_download_file(
|
|
666
|
+
path, http_options=http_options
|
|
553
667
|
)
|
|
554
|
-
result.status_code = e.response.status_code
|
|
555
|
-
raise e
|
|
556
668
|
self._record_interaction(request, result)
|
|
557
669
|
return result
|
|
558
670
|
else:
|
google/genai/_test_api_client.py
CHANGED