google-genai 1.40.0__py3-none-any.whl → 1.42.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/_api_client.py +2 -1
- google/genai/_common.py +213 -77
- google/genai/_extra_utils.py +72 -1
- google/genai/_live_converters.py +729 -3078
- google/genai/_replay_api_client.py +8 -4
- google/genai/_tokens_converters.py +20 -424
- google/genai/_transformers.py +42 -12
- google/genai/batches.py +113 -1063
- google/genai/caches.py +67 -863
- google/genai/errors.py +9 -2
- google/genai/files.py +29 -268
- google/genai/live.py +10 -11
- google/genai/live_music.py +24 -27
- google/genai/models.py +322 -1835
- google/genai/operations.py +6 -32
- google/genai/tokens.py +2 -12
- google/genai/tunings.py +24 -197
- google/genai/types.py +187 -5
- google/genai/version.py +1 -1
- {google_genai-1.40.0.dist-info → google_genai-1.42.0.dist-info}/METADATA +40 -38
- google_genai-1.42.0.dist-info/RECORD +39 -0
- google_genai-1.40.0.dist-info/RECORD +0 -39
- {google_genai-1.40.0.dist-info → google_genai-1.42.0.dist-info}/WHEEL +0 -0
- {google_genai-1.40.0.dist-info → google_genai-1.42.0.dist-info}/licenses/LICENSE +0 -0
- {google_genai-1.40.0.dist-info → google_genai-1.42.0.dist-info}/top_level.txt +0 -0
google/genai/_api_client.py
CHANGED
@@ -80,7 +80,7 @@ if TYPE_CHECKING:
|
|
80
80
|
|
81
81
|
logger = logging.getLogger('google_genai._api_client')
|
82
82
|
CHUNK_SIZE = 8 * 1024 * 1024 # 8 MB chunk size
|
83
|
-
READ_BUFFER_SIZE = 2**
|
83
|
+
READ_BUFFER_SIZE = 2**22
|
84
84
|
MAX_RETRY_COUNT = 3
|
85
85
|
INITIAL_RETRY_DELAY = 1 # second
|
86
86
|
DELAY_MULTIPLIER = 2
|
@@ -489,6 +489,7 @@ def retry_args(options: Optional[HttpRetryOptions]) -> _common.StringDict:
|
|
489
489
|
'retry': retry,
|
490
490
|
'reraise': True,
|
491
491
|
'wait': wait,
|
492
|
+
'before_sleep': tenacity.before_sleep_log(logger, logging.INFO),
|
492
493
|
}
|
493
494
|
|
494
495
|
|
google/genai/_common.py
CHANGED
@@ -21,6 +21,7 @@ import datetime
|
|
21
21
|
import enum
|
22
22
|
import functools
|
23
23
|
import logging
|
24
|
+
import re
|
24
25
|
import typing
|
25
26
|
from typing import Any, Callable, FrozenSet, Optional, Union, get_args, get_origin
|
26
27
|
import uuid
|
@@ -38,7 +39,9 @@ class ExperimentalWarning(Warning):
|
|
38
39
|
"""Warning for experimental features."""
|
39
40
|
|
40
41
|
|
41
|
-
def set_value_by_path(
|
42
|
+
def set_value_by_path(
|
43
|
+
data: Optional[dict[Any, Any]], keys: list[str], value: Any
|
44
|
+
) -> None:
|
42
45
|
"""Examples:
|
43
46
|
|
44
47
|
set_value_by_path({}, ['a', 'b'], v)
|
@@ -100,14 +103,19 @@ def set_value_by_path(data: Optional[dict[Any, Any]], keys: list[str], value: An
|
|
100
103
|
f' Existing value: {existing_data}; New value: {value}.'
|
101
104
|
)
|
102
105
|
else:
|
103
|
-
if (
|
104
|
-
|
106
|
+
if (
|
107
|
+
keys[-1] == '_self'
|
108
|
+
and isinstance(data, dict)
|
109
|
+
and isinstance(value, dict)
|
110
|
+
):
|
105
111
|
data.update(value)
|
106
112
|
else:
|
107
113
|
data[keys[-1]] = value
|
108
114
|
|
109
115
|
|
110
|
-
def get_value_by_path(
|
116
|
+
def get_value_by_path(
|
117
|
+
data: Any, keys: list[str], *, default_value: Any = None
|
118
|
+
) -> Any:
|
111
119
|
"""Examples:
|
112
120
|
|
113
121
|
get_value_by_path({'a': {'b': v}}, ['a', 'b'])
|
@@ -119,36 +127,141 @@ def get_value_by_path(data: Any, keys: list[str]) -> Any:
|
|
119
127
|
return data
|
120
128
|
for i, key in enumerate(keys):
|
121
129
|
if not data:
|
122
|
-
return
|
130
|
+
return default_value
|
123
131
|
if key.endswith('[]'):
|
124
132
|
key_name = key[:-2]
|
125
133
|
if key_name in data:
|
126
|
-
return [
|
134
|
+
return [
|
135
|
+
get_value_by_path(d, keys[i + 1 :], default_value=default_value)
|
136
|
+
for d in data[key_name]
|
137
|
+
]
|
127
138
|
else:
|
128
|
-
return
|
139
|
+
return default_value
|
129
140
|
elif key.endswith('[0]'):
|
130
141
|
key_name = key[:-3]
|
131
142
|
if key_name in data and data[key_name]:
|
132
|
-
return get_value_by_path(
|
143
|
+
return get_value_by_path(
|
144
|
+
data[key_name][0], keys[i + 1 :], default_value=default_value
|
145
|
+
)
|
133
146
|
else:
|
134
|
-
return
|
147
|
+
return default_value
|
135
148
|
else:
|
136
149
|
if key in data:
|
137
150
|
data = data[key]
|
138
151
|
elif isinstance(data, BaseModel) and hasattr(data, key):
|
139
152
|
data = getattr(data, key)
|
140
153
|
else:
|
141
|
-
return
|
154
|
+
return default_value
|
142
155
|
return data
|
143
156
|
|
144
157
|
|
145
|
-
def
|
158
|
+
def move_value_by_path(data: Any, paths: dict[str, str]) -> None:
|
159
|
+
"""Moves values from source paths to destination paths.
|
160
|
+
|
161
|
+
Examples:
|
162
|
+
move_value_by_path(
|
163
|
+
{'requests': [{'content': v1}, {'content': v2}]},
|
164
|
+
{'requests[].*': 'requests[].request.*'}
|
165
|
+
)
|
166
|
+
-> {'requests': [{'request': {'content': v1}}, {'request': {'content':
|
167
|
+
v2}}]}
|
168
|
+
"""
|
169
|
+
for source_path, dest_path in paths.items():
|
170
|
+
source_keys = source_path.split('.')
|
171
|
+
dest_keys = dest_path.split('.')
|
172
|
+
|
173
|
+
# Determine keys to exclude from wildcard to avoid cyclic references
|
174
|
+
exclude_keys = set()
|
175
|
+
wildcard_idx = -1
|
176
|
+
for i, key in enumerate(source_keys):
|
177
|
+
if key == '*':
|
178
|
+
wildcard_idx = i
|
179
|
+
break
|
180
|
+
|
181
|
+
if wildcard_idx != -1 and len(dest_keys) > wildcard_idx:
|
182
|
+
# Extract the intermediate key between source and dest paths
|
183
|
+
# Example: source=['requests[]', '*'], dest=['requests[]', 'request', '*']
|
184
|
+
# We want to exclude 'request'
|
185
|
+
for i in range(wildcard_idx, len(dest_keys)):
|
186
|
+
key = dest_keys[i]
|
187
|
+
if key != '*' and not key.endswith('[]') and not key.endswith('[0]'):
|
188
|
+
exclude_keys.add(key)
|
189
|
+
|
190
|
+
# Move values recursively
|
191
|
+
_move_value_recursive(data, source_keys, dest_keys, 0, exclude_keys)
|
192
|
+
|
193
|
+
|
194
|
+
def _move_value_recursive(
|
195
|
+
data: Any,
|
196
|
+
source_keys: list[str],
|
197
|
+
dest_keys: list[str],
|
198
|
+
key_idx: int,
|
199
|
+
exclude_keys: set[str],
|
200
|
+
) -> None:
|
201
|
+
"""Recursively moves values from source path to destination path."""
|
202
|
+
if key_idx >= len(source_keys):
|
203
|
+
return
|
204
|
+
|
205
|
+
key = source_keys[key_idx]
|
206
|
+
|
207
|
+
if key.endswith('[]'):
|
208
|
+
# Handle array iteration
|
209
|
+
key_name = key[:-2]
|
210
|
+
if key_name in data and isinstance(data[key_name], list):
|
211
|
+
for item in data[key_name]:
|
212
|
+
_move_value_recursive(
|
213
|
+
item, source_keys, dest_keys, key_idx + 1, exclude_keys
|
214
|
+
)
|
215
|
+
elif key == '*':
|
216
|
+
# Handle wildcard - move all fields
|
217
|
+
if isinstance(data, dict):
|
218
|
+
# Get all keys to move (excluding specified keys)
|
219
|
+
keys_to_move = [
|
220
|
+
k
|
221
|
+
for k in list(data.keys())
|
222
|
+
if not k.startswith('_') and k not in exclude_keys
|
223
|
+
]
|
224
|
+
|
225
|
+
# Collect values to move
|
226
|
+
values_to_move = {k: data[k] for k in keys_to_move}
|
227
|
+
|
228
|
+
# Set values at destination
|
229
|
+
for k, v in values_to_move.items():
|
230
|
+
# Build destination keys with the field name
|
231
|
+
new_dest_keys = []
|
232
|
+
for dk in dest_keys[key_idx:]:
|
233
|
+
if dk == '*':
|
234
|
+
new_dest_keys.append(k)
|
235
|
+
else:
|
236
|
+
new_dest_keys.append(dk)
|
237
|
+
set_value_by_path(data, new_dest_keys, v)
|
238
|
+
|
239
|
+
# Delete from source
|
240
|
+
for k in keys_to_move:
|
241
|
+
del data[k]
|
242
|
+
else:
|
243
|
+
# Navigate to next level
|
244
|
+
if key in data:
|
245
|
+
_move_value_recursive(
|
246
|
+
data[key], source_keys, dest_keys, key_idx + 1, exclude_keys
|
247
|
+
)
|
248
|
+
|
249
|
+
|
250
|
+
def maybe_snake_to_camel(snake_str: str, convert: bool = True) -> str:
|
251
|
+
"""Converts a snake_case string to CamelCase, if convert is True."""
|
252
|
+
if not convert:
|
253
|
+
return snake_str
|
254
|
+
return re.sub(r'_([a-zA-Z])', lambda match: match.group(1).upper(), snake_str)
|
255
|
+
|
256
|
+
|
257
|
+
def convert_to_dict(obj: object, convert_keys: bool = False) -> Any:
|
146
258
|
"""Recursively converts a given object to a dictionary.
|
147
259
|
|
148
260
|
If the object is a Pydantic model, it uses the model's `model_dump()` method.
|
149
261
|
|
150
262
|
Args:
|
151
263
|
obj: The object to convert.
|
264
|
+
convert_keys: Whether to convert the keys from snake case to camel case.
|
152
265
|
|
153
266
|
Returns:
|
154
267
|
A dictionary representation of the object, a list of objects if a list is
|
@@ -156,17 +269,23 @@ def convert_to_dict(obj: object) -> Any:
|
|
156
269
|
model.
|
157
270
|
"""
|
158
271
|
if isinstance(obj, pydantic.BaseModel):
|
159
|
-
return obj.model_dump(exclude_none=True)
|
272
|
+
return convert_to_dict(obj.model_dump(exclude_none=True), convert_keys)
|
160
273
|
elif isinstance(obj, dict):
|
161
|
-
return {
|
274
|
+
return {
|
275
|
+
maybe_snake_to_camel(key, convert_keys): convert_to_dict(
|
276
|
+
value, convert_keys
|
277
|
+
)
|
278
|
+
for key, value in obj.items()
|
279
|
+
}
|
162
280
|
elif isinstance(obj, list):
|
163
|
-
return [convert_to_dict(item) for item in obj]
|
281
|
+
return [convert_to_dict(item, convert_keys) for item in obj]
|
164
282
|
else:
|
165
283
|
return obj
|
166
284
|
|
167
285
|
|
168
286
|
def _is_struct_type(annotation: type) -> bool:
|
169
287
|
"""Checks if the given annotation is list[dict[str, typing.Any]]
|
288
|
+
|
170
289
|
or typing.List[typing.Dict[str, typing.Any]].
|
171
290
|
|
172
291
|
This maps to Struct type in the API.
|
@@ -174,7 +293,7 @@ def _is_struct_type(annotation: type) -> bool:
|
|
174
293
|
outer_origin = get_origin(annotation)
|
175
294
|
outer_args = get_args(annotation)
|
176
295
|
|
177
|
-
if outer_origin is not list:
|
296
|
+
if outer_origin is not list: # Python 3.9+ normalizes list
|
178
297
|
return False
|
179
298
|
|
180
299
|
if not outer_args or len(outer_args) != 1:
|
@@ -185,7 +304,7 @@ def _is_struct_type(annotation: type) -> bool:
|
|
185
304
|
inner_origin = get_origin(inner_annotation)
|
186
305
|
inner_args = get_args(inner_annotation)
|
187
306
|
|
188
|
-
if inner_origin is not dict:
|
307
|
+
if inner_origin is not dict: # Python 3.9+ normalizes to dict
|
189
308
|
return False
|
190
309
|
|
191
310
|
if not inner_args or len(inner_args) != 2:
|
@@ -197,9 +316,7 @@ def _is_struct_type(annotation: type) -> bool:
|
|
197
316
|
return key_type is str and value_type is typing.Any
|
198
317
|
|
199
318
|
|
200
|
-
def _remove_extra_fields(
|
201
|
-
model: Any, response: dict[str, object]
|
202
|
-
) -> None:
|
319
|
+
def _remove_extra_fields(model: Any, response: dict[str, object]) -> None:
|
203
320
|
"""Removes extra fields from the response that are not in the model.
|
204
321
|
|
205
322
|
Mutates the response in place.
|
@@ -239,6 +356,7 @@ def _remove_extra_fields(
|
|
239
356
|
if isinstance(item, dict):
|
240
357
|
_remove_extra_fields(typing.get_args(annotation)[0], item)
|
241
358
|
|
359
|
+
|
242
360
|
T = typing.TypeVar('T', bound='BaseModel')
|
243
361
|
|
244
362
|
|
@@ -378,56 +496,57 @@ def _format_collection(
|
|
378
496
|
depth: int,
|
379
497
|
visited: FrozenSet[int],
|
380
498
|
) -> str:
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
else:
|
393
|
-
brackets = ('set(', ')')
|
499
|
+
"""Formats a collection (list, tuple, set)."""
|
500
|
+
if isinstance(obj, list):
|
501
|
+
brackets = ('[', ']')
|
502
|
+
internal_obj = obj
|
503
|
+
elif isinstance(obj, tuple):
|
504
|
+
brackets = ('(', ')')
|
505
|
+
internal_obj = list(obj)
|
506
|
+
elif isinstance(obj, set):
|
507
|
+
internal_obj = list(obj)
|
508
|
+
if obj:
|
509
|
+
brackets = ('{', '}')
|
394
510
|
else:
|
395
|
-
|
511
|
+
brackets = ('set(', ')')
|
512
|
+
else:
|
513
|
+
raise ValueError(f'Unsupported collection type: {type(obj)}')
|
396
514
|
|
397
|
-
|
398
|
-
|
515
|
+
if not internal_obj:
|
516
|
+
return brackets[0] + brackets[1]
|
399
517
|
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
indent = ' ' * indent_level
|
406
|
-
next_indent_str = ' ' * (indent_level + indent_delta)
|
407
|
-
elements = []
|
408
|
-
num_to_show = min(len(internal_obj), max_items)
|
409
|
-
|
410
|
-
for i in range(num_to_show):
|
411
|
-
elem = internal_obj[i]
|
412
|
-
elements.append(
|
413
|
-
next_indent_str
|
414
|
-
+ _pretty_repr(
|
415
|
-
elem,
|
416
|
-
indent_level=indent_level + indent_delta,
|
417
|
-
indent_delta=indent_delta,
|
418
|
-
max_len=max_len,
|
419
|
-
max_items=max_items,
|
420
|
-
depth=depth - 1,
|
421
|
-
visited=visited,
|
422
|
-
)
|
423
|
-
)
|
518
|
+
# If the call to _pretty_repr for elements will have depth < 0
|
519
|
+
if depth <= 0:
|
520
|
+
item_count_str = f"{len(internal_obj)} item{'s'*(len(internal_obj)!=1)}"
|
521
|
+
return f'{brackets[0]}<... {item_count_str} at Max depth ...>{brackets[1]}'
|
424
522
|
|
425
|
-
|
426
|
-
|
427
|
-
|
523
|
+
indent = ' ' * indent_level
|
524
|
+
next_indent_str = ' ' * (indent_level + indent_delta)
|
525
|
+
elements = []
|
526
|
+
num_to_show = min(len(internal_obj), max_items)
|
527
|
+
|
528
|
+
for i in range(num_to_show):
|
529
|
+
elem = internal_obj[i]
|
530
|
+
elements.append(
|
531
|
+
next_indent_str
|
532
|
+
+ _pretty_repr(
|
533
|
+
elem,
|
534
|
+
indent_level=indent_level + indent_delta,
|
535
|
+
indent_delta=indent_delta,
|
536
|
+
max_len=max_len,
|
537
|
+
max_items=max_items,
|
538
|
+
depth=depth - 1,
|
539
|
+
visited=visited,
|
428
540
|
)
|
541
|
+
)
|
542
|
+
|
543
|
+
if len(internal_obj) > max_items:
|
544
|
+
elements.append(
|
545
|
+
f'{next_indent_str}<... {len(internal_obj) - max_items} more items ...>'
|
546
|
+
)
|
547
|
+
|
548
|
+
return f'{brackets[0]}\n' + ',\n'.join(elements) + f',\n{indent}{brackets[1]}'
|
429
549
|
|
430
|
-
return f'{brackets[0]}\n' + ',\n'.join(elements) + f',\n{indent}{brackets[1]}'
|
431
550
|
|
432
551
|
class BaseModel(pydantic.BaseModel):
|
433
552
|
|
@@ -441,7 +560,7 @@ class BaseModel(pydantic.BaseModel):
|
|
441
560
|
arbitrary_types_allowed=True,
|
442
561
|
ser_json_bytes='base64',
|
443
562
|
val_json_bytes='base64',
|
444
|
-
ignored_types=(typing.TypeVar,)
|
563
|
+
ignored_types=(typing.TypeVar,),
|
445
564
|
)
|
446
565
|
|
447
566
|
def __repr__(self) -> str:
|
@@ -452,7 +571,10 @@ class BaseModel(pydantic.BaseModel):
|
|
452
571
|
|
453
572
|
@classmethod
|
454
573
|
def _from_response(
|
455
|
-
cls: typing.Type[T],
|
574
|
+
cls: typing.Type[T],
|
575
|
+
*,
|
576
|
+
response: dict[str, object],
|
577
|
+
kwargs: dict[str, object],
|
456
578
|
) -> T:
|
457
579
|
# To maintain forward compatibility, we need to remove extra fields from
|
458
580
|
# the response.
|
@@ -462,11 +584,11 @@ class BaseModel(pydantic.BaseModel):
|
|
462
584
|
# user may pass a dict that is not a subclass of BaseModel.
|
463
585
|
# If more modules require we skip this, we may want a different approach
|
464
586
|
should_skip_removing_fields = (
|
465
|
-
kwargs is not None
|
466
|
-
'config' in kwargs
|
467
|
-
kwargs['config'] is not None
|
468
|
-
isinstance(kwargs['config'], dict)
|
469
|
-
'include_all_fields' in kwargs['config']
|
587
|
+
kwargs is not None
|
588
|
+
and 'config' in kwargs
|
589
|
+
and kwargs['config'] is not None
|
590
|
+
and isinstance(kwargs['config'], dict)
|
591
|
+
and 'include_all_fields' in kwargs['config']
|
470
592
|
and kwargs['config']['include_all_fields']
|
471
593
|
)
|
472
594
|
|
@@ -490,7 +612,7 @@ class CaseInSensitiveEnum(str, enum.Enum):
|
|
490
612
|
try:
|
491
613
|
return cls[value.lower()] # Try to access directly with lowercase
|
492
614
|
except KeyError:
|
493
|
-
warnings.warn(f
|
615
|
+
warnings.warn(f'{value} is not a valid {cls.__name__}')
|
494
616
|
try:
|
495
617
|
# Creating a enum instance based on the value
|
496
618
|
# We need to use super() to avoid infinite recursion.
|
@@ -551,10 +673,14 @@ def encode_unserializable_types(data: dict[str, object]) -> dict[str, object]:
|
|
551
673
|
return processed_data
|
552
674
|
|
553
675
|
|
554
|
-
def experimental_warning(
|
676
|
+
def experimental_warning(
|
677
|
+
message: str,
|
678
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
555
679
|
"""Experimental warning, only warns once."""
|
680
|
+
|
556
681
|
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
557
682
|
warning_done = False
|
683
|
+
|
558
684
|
@functools.wraps(func)
|
559
685
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
560
686
|
nonlocal warning_done
|
@@ -566,13 +692,15 @@ def experimental_warning(message: str) -> Callable[[Callable[..., Any]], Callabl
|
|
566
692
|
stacklevel=2,
|
567
693
|
)
|
568
694
|
return func(*args, **kwargs)
|
695
|
+
|
569
696
|
return wrapper
|
697
|
+
|
570
698
|
return decorator
|
571
699
|
|
572
700
|
|
573
701
|
def _normalize_key_for_matching(key_str: str) -> str:
|
574
702
|
"""Normalizes a key for case-insensitive and snake/camel matching."""
|
575
|
-
return key_str.replace(
|
703
|
+
return key_str.replace('_', '').lower()
|
576
704
|
|
577
705
|
|
578
706
|
def align_key_case(
|
@@ -588,7 +716,9 @@ def align_key_case(
|
|
588
716
|
A new dictionary with keys aligned to target_dict's key casing.
|
589
717
|
"""
|
590
718
|
aligned_update_dict: StringDict = {}
|
591
|
-
target_keys_map = {
|
719
|
+
target_keys_map = {
|
720
|
+
_normalize_key_for_matching(key): key for key in target_dict.keys()
|
721
|
+
}
|
592
722
|
|
593
723
|
for key, value in update_dict.items():
|
594
724
|
normalized_update_key = _normalize_key_for_matching(key)
|
@@ -598,9 +728,15 @@ def align_key_case(
|
|
598
728
|
else:
|
599
729
|
aligned_key = key
|
600
730
|
|
601
|
-
if isinstance(value, dict) and isinstance(
|
602
|
-
|
603
|
-
|
731
|
+
if isinstance(value, dict) and isinstance(
|
732
|
+
target_dict.get(aligned_key), dict
|
733
|
+
):
|
734
|
+
aligned_update_dict[aligned_key] = align_key_case(
|
735
|
+
target_dict[aligned_key], value
|
736
|
+
)
|
737
|
+
elif isinstance(value, list) and isinstance(
|
738
|
+
target_dict.get(aligned_key), list
|
739
|
+
):
|
604
740
|
# Direct assign as we treat update_dict list values as golden source.
|
605
741
|
aligned_update_dict[aligned_key] = value
|
606
742
|
else:
|
google/genai/_extra_utils.py
CHANGED
@@ -16,11 +16,13 @@
|
|
16
16
|
"""Extra utils depending on types that are shared between sync and async modules."""
|
17
17
|
|
18
18
|
import inspect
|
19
|
+
import io
|
19
20
|
import logging
|
20
21
|
import sys
|
21
22
|
import typing
|
22
23
|
from typing import Any, Callable, Dict, Optional, Union, get_args, get_origin
|
23
|
-
|
24
|
+
import mimetypes
|
25
|
+
import os
|
24
26
|
import pydantic
|
25
27
|
|
26
28
|
from . import _common
|
@@ -541,3 +543,72 @@ def append_chunk_contents(
|
|
541
543
|
contents = t.t_contents(contents) # type: ignore[assignment]
|
542
544
|
if isinstance(contents, list) and chunk_content is not None:
|
543
545
|
contents.append(chunk_content) # type: ignore[arg-type]
|
546
|
+
|
547
|
+
|
548
|
+
def prepare_resumable_upload(
|
549
|
+
file: Union[str, os.PathLike[str], io.IOBase],
|
550
|
+
user_http_options: Optional[types.HttpOptionsOrDict] = None,
|
551
|
+
user_mime_type: Optional[str] = None,
|
552
|
+
) -> tuple[
|
553
|
+
types.HttpOptions,
|
554
|
+
int,
|
555
|
+
str,
|
556
|
+
]:
|
557
|
+
"""Prepares the HTTP options, file bytes size and mime type for a resumable upload.
|
558
|
+
|
559
|
+
This function inspects a file (from a path or an in-memory object) to
|
560
|
+
determine its size and MIME type. It then constructs the necessary HTTP
|
561
|
+
headers and options required to initiate a resumable upload session.
|
562
|
+
"""
|
563
|
+
size_bytes = None
|
564
|
+
mime_type = user_mime_type
|
565
|
+
if isinstance(file, io.IOBase):
|
566
|
+
if mime_type is None:
|
567
|
+
raise ValueError(
|
568
|
+
'Unknown mime type: Could not determine the mimetype for your'
|
569
|
+
' file\n please set the `mime_type` argument'
|
570
|
+
)
|
571
|
+
if hasattr(file, 'mode'):
|
572
|
+
if 'b' not in file.mode:
|
573
|
+
raise ValueError('The file must be opened in binary mode.')
|
574
|
+
offset = file.tell()
|
575
|
+
file.seek(0, os.SEEK_END)
|
576
|
+
size_bytes = file.tell() - offset
|
577
|
+
file.seek(offset, os.SEEK_SET)
|
578
|
+
else:
|
579
|
+
fs_path = os.fspath(file)
|
580
|
+
if not fs_path or not os.path.isfile(fs_path):
|
581
|
+
raise FileNotFoundError(f'{file} is not a valid file path.')
|
582
|
+
size_bytes = os.path.getsize(fs_path)
|
583
|
+
if mime_type is None:
|
584
|
+
mime_type, _ = mimetypes.guess_type(fs_path)
|
585
|
+
if mime_type is None:
|
586
|
+
raise ValueError(
|
587
|
+
'Unknown mime type: Could not determine the mimetype for your'
|
588
|
+
' file\n please set the `mime_type` argument'
|
589
|
+
)
|
590
|
+
http_options: types.HttpOptions
|
591
|
+
if user_http_options:
|
592
|
+
if isinstance(user_http_options, dict):
|
593
|
+
user_http_options = types.HttpOptions(**user_http_options)
|
594
|
+
http_options = user_http_options
|
595
|
+
http_options.api_version = ''
|
596
|
+
http_options.headers = {
|
597
|
+
'Content-Type': 'application/json',
|
598
|
+
'X-Goog-Upload-Protocol': 'resumable',
|
599
|
+
'X-Goog-Upload-Command': 'start',
|
600
|
+
'X-Goog-Upload-Header-Content-Length': f'{size_bytes}',
|
601
|
+
'X-Goog-Upload-Header-Content-Type': f'{mime_type}',
|
602
|
+
}
|
603
|
+
else:
|
604
|
+
http_options = types.HttpOptions(
|
605
|
+
api_version='',
|
606
|
+
headers={
|
607
|
+
'Content-Type': 'application/json',
|
608
|
+
'X-Goog-Upload-Protocol': 'resumable',
|
609
|
+
'X-Goog-Upload-Command': 'start',
|
610
|
+
'X-Goog-Upload-Header-Content-Length': f'{size_bytes}',
|
611
|
+
'X-Goog-Upload-Header-Content-Type': f'{mime_type}',
|
612
|
+
},
|
613
|
+
)
|
614
|
+
return http_options, size_bytes, mime_type
|