gemini-webapi 1.17.3__py3-none-any.whl → 1.18.1__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.
- gemini_webapi/client.py +663 -306
- gemini_webapi/components/gem_mixin.py +46 -23
- gemini_webapi/constants.py +12 -8
- gemini_webapi/types/candidate.py +2 -0
- gemini_webapi/types/image.py +8 -7
- gemini_webapi/types/modeloutput.py +8 -0
- gemini_webapi/utils/__init__.py +1 -6
- gemini_webapi/utils/decorators.py +75 -30
- gemini_webapi/utils/get_access_token.py +55 -34
- gemini_webapi/utils/parsing.py +207 -37
- gemini_webapi/utils/rotate_1psidts.py +40 -21
- gemini_webapi/utils/upload_file.py +51 -18
- {gemini_webapi-1.17.3.dist-info → gemini_webapi-1.18.1.dist-info}/METADATA +52 -10
- gemini_webapi-1.18.1.dist-info/RECORD +25 -0
- {gemini_webapi-1.17.3.dist-info → gemini_webapi-1.18.1.dist-info}/WHEEL +1 -1
- gemini_webapi-1.17.3.dist-info/RECORD +0 -25
- {gemini_webapi-1.17.3.dist-info → gemini_webapi-1.18.1.dist-info}/licenses/LICENSE +0 -0
- {gemini_webapi-1.17.3.dist-info → gemini_webapi-1.18.1.dist-info}/top_level.txt +0 -0
gemini_webapi/utils/parsing.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import reprlib
|
|
1
3
|
from typing import Any
|
|
2
4
|
|
|
3
5
|
import orjson as json
|
|
@@ -5,63 +7,201 @@ import orjson as json
|
|
|
5
7
|
from .logger import logger
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
_LENGTH_MARKER_PATTERN = re.compile(r"(\d+)\n")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_char_count_for_utf16_units(
|
|
14
|
+
s: str, start_idx: int, utf16_units: int
|
|
15
|
+
) -> tuple[int, int]:
|
|
16
|
+
"""
|
|
17
|
+
Calculate the number of Python characters (code points) and actual UTF-16
|
|
18
|
+
units found.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
count = 0
|
|
22
|
+
units = 0
|
|
23
|
+
limit = len(s)
|
|
24
|
+
|
|
25
|
+
while units < utf16_units and (start_idx + count) < limit:
|
|
26
|
+
char = s[start_idx + count]
|
|
27
|
+
u = 2 if ord(char) > 0xFFFF else 1
|
|
28
|
+
if units + u > utf16_units:
|
|
29
|
+
break
|
|
30
|
+
units += u
|
|
31
|
+
count += 1
|
|
32
|
+
|
|
33
|
+
return count, units
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_nested_value(
|
|
37
|
+
data: Any, path: list[int | str], default: Any = None, verbose: bool = False
|
|
38
|
+
) -> Any:
|
|
9
39
|
"""
|
|
10
|
-
Safely
|
|
40
|
+
Safely navigate through a nested structure (list or dict) using a sequence of keys/indices.
|
|
11
41
|
|
|
12
42
|
Parameters
|
|
13
43
|
----------
|
|
14
|
-
data: `
|
|
15
|
-
The nested
|
|
16
|
-
path: `list[int]`
|
|
17
|
-
A list of indices representing the path
|
|
18
|
-
default: `Any
|
|
19
|
-
|
|
44
|
+
data: `Any`
|
|
45
|
+
The nested structure to traverse.
|
|
46
|
+
path: `list[int | str]`
|
|
47
|
+
A list of indices or keys representing the path.
|
|
48
|
+
default: `Any`
|
|
49
|
+
Value to return if the path is invalid.
|
|
50
|
+
verbose: `bool`
|
|
51
|
+
If True, log debug information when the path cannot be fully traversed.
|
|
20
52
|
"""
|
|
21
53
|
|
|
22
54
|
current = data
|
|
23
55
|
|
|
24
56
|
for i, key in enumerate(path):
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
57
|
+
found = False
|
|
58
|
+
if isinstance(key, int):
|
|
59
|
+
if isinstance(current, list) and -len(current) <= key < len(current):
|
|
60
|
+
current = current[key]
|
|
61
|
+
found = True
|
|
62
|
+
elif isinstance(key, str):
|
|
63
|
+
if isinstance(current, dict) and key in current:
|
|
64
|
+
current = current[key]
|
|
65
|
+
found = True
|
|
66
|
+
|
|
67
|
+
if not found:
|
|
68
|
+
if verbose:
|
|
69
|
+
logger.debug(
|
|
70
|
+
f"Safe navigation: path {path} ended at index {i} (key '{key}'), "
|
|
71
|
+
f"returning default. Context: {reprlib.repr(current)}"
|
|
72
|
+
)
|
|
73
|
+
return default
|
|
74
|
+
|
|
75
|
+
return current if current is not None else default
|
|
31
76
|
|
|
77
|
+
|
|
78
|
+
def _parse_with_length_markers(content: str) -> list | None:
|
|
79
|
+
"""
|
|
80
|
+
Parse streaming responses using length markers as hints.
|
|
81
|
+
Google's format: [length]\n[json_payload]\n
|
|
82
|
+
The length value includes the newline after the number and the newline after the JSON.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
pos = 0
|
|
86
|
+
total_len = len(content)
|
|
87
|
+
collected_chunks = []
|
|
88
|
+
|
|
89
|
+
while pos < total_len:
|
|
90
|
+
while pos < total_len and content[pos].isspace():
|
|
91
|
+
pos += 1
|
|
92
|
+
|
|
93
|
+
if pos >= total_len:
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
match = _LENGTH_MARKER_PATTERN.match(content, pos=pos)
|
|
97
|
+
if not match:
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
length_val = match.group(1)
|
|
101
|
+
length = int(length_val)
|
|
102
|
+
|
|
103
|
+
# Content starts immediately after the digits.
|
|
104
|
+
# Google uses UTF-16 code units (JavaScript .length) for the length marker.
|
|
105
|
+
start_content = match.start() + len(length_val)
|
|
106
|
+
char_count, units_found = _get_char_count_for_utf16_units(
|
|
107
|
+
content, start_content, length
|
|
108
|
+
)
|
|
109
|
+
end_hint = start_content + char_count
|
|
110
|
+
pos = end_hint
|
|
111
|
+
|
|
112
|
+
if units_found < length:
|
|
32
113
|
logger.debug(
|
|
33
|
-
f"
|
|
34
|
-
f"returning default. Context: {current_repr}"
|
|
114
|
+
f"Chunk at pos {start_content} is truncated. Expected {length} UTF-16 units, got {units_found}."
|
|
35
115
|
)
|
|
116
|
+
break
|
|
36
117
|
|
|
37
|
-
|
|
118
|
+
chunk = content[start_content:end_hint].strip()
|
|
119
|
+
if not chunk:
|
|
120
|
+
continue
|
|
38
121
|
|
|
39
|
-
|
|
40
|
-
|
|
122
|
+
try:
|
|
123
|
+
parsed = json.loads(chunk)
|
|
124
|
+
if isinstance(parsed, list):
|
|
125
|
+
collected_chunks.extend(parsed)
|
|
126
|
+
else:
|
|
127
|
+
collected_chunks.append(parsed)
|
|
128
|
+
except json.JSONDecodeError:
|
|
129
|
+
logger.warning(
|
|
130
|
+
f"Failed to parse chunk at pos {start_content} with length {length}. "
|
|
131
|
+
f"Snippet: {reprlib.repr(chunk)}"
|
|
132
|
+
)
|
|
41
133
|
|
|
42
|
-
return
|
|
134
|
+
return collected_chunks if collected_chunks else None
|
|
43
135
|
|
|
44
136
|
|
|
45
|
-
def
|
|
137
|
+
def parse_stream_frames(buffer: str) -> tuple[list[Any], str]:
|
|
46
138
|
"""
|
|
47
|
-
|
|
139
|
+
Parse as many JSON frames as possible from an accumulated buffer.
|
|
140
|
+
|
|
141
|
+
This function implements Google's length-prefixed framing protocol. Each frame starts
|
|
142
|
+
with a length marker (number of characters) followed by a newline and the JSON content.
|
|
143
|
+
If a frame is partially received, it stays in the buffer for the next call.
|
|
48
144
|
|
|
49
145
|
Parameters
|
|
50
146
|
----------
|
|
51
|
-
|
|
52
|
-
The raw
|
|
147
|
+
buffer: `str`
|
|
148
|
+
The accumulated string buffer containing raw streaming data from the API.
|
|
53
149
|
|
|
54
150
|
Returns
|
|
55
151
|
-------
|
|
56
|
-
`list`
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
152
|
+
`tuple[list[Any], str]`
|
|
153
|
+
A tuple containing:
|
|
154
|
+
- A list of parsed JSON objects (envelopes) extracted from the buffer.
|
|
155
|
+
- The remaining unparsed part of the buffer (incomplete frames).
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
pos = 0
|
|
159
|
+
total_len = len(buffer)
|
|
160
|
+
parsed_objects = []
|
|
161
|
+
|
|
162
|
+
while pos < total_len:
|
|
163
|
+
while pos < total_len and buffer[pos].isspace():
|
|
164
|
+
pos += 1
|
|
165
|
+
|
|
166
|
+
if pos >= total_len:
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
match = _LENGTH_MARKER_PATTERN.match(buffer, pos=pos)
|
|
170
|
+
if not match:
|
|
171
|
+
# If we have a prefix but no length marker yet, wait for more data
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
length_val = match.group(1)
|
|
175
|
+
length = int(length_val)
|
|
176
|
+
start_content = match.start() + len(length_val)
|
|
177
|
+
char_count, units_found = _get_char_count_for_utf16_units(
|
|
178
|
+
buffer, start_content, length
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if units_found < length:
|
|
182
|
+
# Chunk is truncated, wait for more data
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
end_pos = start_content + char_count
|
|
186
|
+
chunk = buffer[start_content:end_pos].strip()
|
|
187
|
+
pos = end_pos
|
|
188
|
+
|
|
189
|
+
if chunk:
|
|
190
|
+
try:
|
|
191
|
+
parsed = json.loads(chunk)
|
|
192
|
+
if isinstance(parsed, list):
|
|
193
|
+
parsed_objects.extend(parsed)
|
|
194
|
+
else:
|
|
195
|
+
parsed_objects.append(parsed)
|
|
196
|
+
except json.JSONDecodeError:
|
|
197
|
+
logger.debug(f"Streaming: Failed to parse chunk: {reprlib.repr(chunk)}")
|
|
198
|
+
|
|
199
|
+
return parsed_objects, buffer[pos:]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def extract_json_from_response(text: str) -> list:
|
|
203
|
+
"""
|
|
204
|
+
Extract and normalize JSON content from a Google API response.
|
|
65
205
|
"""
|
|
66
206
|
|
|
67
207
|
if not isinstance(text, str):
|
|
@@ -69,12 +209,42 @@ def extract_json_from_response(text: str) -> list:
|
|
|
69
209
|
f"Input text is expected to be a string, got {type(text).__name__} instead."
|
|
70
210
|
)
|
|
71
211
|
|
|
72
|
-
|
|
73
|
-
|
|
212
|
+
content = text
|
|
213
|
+
if content.startswith(")]}'"):
|
|
214
|
+
content = content[4:]
|
|
215
|
+
|
|
216
|
+
content = content.lstrip()
|
|
217
|
+
|
|
218
|
+
# Extract with a length marker
|
|
219
|
+
result = _parse_with_length_markers(content)
|
|
220
|
+
if result is not None:
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
# Extract the entire content
|
|
224
|
+
content_stripped = content.strip()
|
|
225
|
+
try:
|
|
226
|
+
parsed = json.loads(content_stripped)
|
|
227
|
+
return parsed if isinstance(parsed, list) else [parsed]
|
|
228
|
+
except json.JSONDecodeError:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
# Extract with NDJSON
|
|
232
|
+
collected_lines = []
|
|
233
|
+
for line in content_stripped.splitlines():
|
|
234
|
+
line = line.strip()
|
|
235
|
+
if not line:
|
|
236
|
+
continue
|
|
74
237
|
try:
|
|
75
|
-
|
|
238
|
+
parsed = json.loads(line)
|
|
76
239
|
except json.JSONDecodeError:
|
|
77
240
|
continue
|
|
78
241
|
|
|
79
|
-
|
|
242
|
+
if isinstance(parsed, list):
|
|
243
|
+
collected_lines.extend(parsed)
|
|
244
|
+
elif isinstance(parsed, dict):
|
|
245
|
+
collected_lines.append(parsed)
|
|
246
|
+
|
|
247
|
+
if collected_lines:
|
|
248
|
+
return collected_lines
|
|
249
|
+
|
|
80
250
|
raise ValueError("Could not find a valid JSON object or array in the response.")
|
|
@@ -2,27 +2,29 @@ import os
|
|
|
2
2
|
import time
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from httpx import AsyncClient
|
|
5
|
+
from httpx import AsyncClient, Cookies
|
|
6
6
|
|
|
7
7
|
from ..constants import Endpoint, Headers
|
|
8
8
|
from ..exceptions import AuthError
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
async def rotate_1psidts(
|
|
11
|
+
async def rotate_1psidts(
|
|
12
|
+
cookies: dict | Cookies, proxy: str | None = None
|
|
13
|
+
) -> tuple[str | None, Cookies | None]:
|
|
12
14
|
"""
|
|
13
15
|
Refresh the __Secure-1PSIDTS cookie and store the refreshed cookie value in cache file.
|
|
14
16
|
|
|
15
17
|
Parameters
|
|
16
18
|
----------
|
|
17
|
-
cookies : `dict`
|
|
19
|
+
cookies : `dict | httpx.Cookies`
|
|
18
20
|
Cookies to be used in the request.
|
|
19
21
|
proxy: `str`, optional
|
|
20
22
|
Proxy URL.
|
|
21
23
|
|
|
22
24
|
Returns
|
|
23
25
|
-------
|
|
24
|
-
`str`
|
|
25
|
-
New value of the __Secure-1PSIDTS cookie.
|
|
26
|
+
`tuple[str | None, httpx.Cookies | None]`
|
|
27
|
+
New value of the __Secure-1PSIDTS cookie and the full updated cookies jar.
|
|
26
28
|
|
|
27
29
|
Raises
|
|
28
30
|
------
|
|
@@ -38,22 +40,39 @@ async def rotate_1psidts(cookies: dict, proxy: str | None = None) -> str:
|
|
|
38
40
|
or (Path(__file__).parent / "temp")
|
|
39
41
|
)
|
|
40
42
|
path.mkdir(parents=True, exist_ok=True)
|
|
41
|
-
|
|
43
|
+
|
|
44
|
+
# Safely get __Secure-1PSID value for filename
|
|
45
|
+
if isinstance(cookies, Cookies):
|
|
46
|
+
# Prefer .google.com domain to avoid CookieConflict
|
|
47
|
+
secure_1psid = cookies.get(
|
|
48
|
+
"__Secure-1PSID", domain=".google.com"
|
|
49
|
+
) or cookies.get("__Secure-1PSID")
|
|
50
|
+
else:
|
|
51
|
+
secure_1psid = cookies.get("__Secure-1PSID")
|
|
52
|
+
|
|
53
|
+
if not secure_1psid:
|
|
54
|
+
return None, None
|
|
55
|
+
|
|
56
|
+
filename = f".cached_1psidts_{secure_1psid}.txt"
|
|
42
57
|
path = path / filename
|
|
43
58
|
|
|
44
59
|
# Check if the cache file was modified in the last minute to avoid 429 Too Many Requests
|
|
45
|
-
if
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
if path.is_file() and time.time() - os.path.getmtime(path) <= 60:
|
|
61
|
+
return path.read_text(), None
|
|
62
|
+
|
|
63
|
+
async with AsyncClient(http2=True, proxy=proxy) as client:
|
|
64
|
+
response = await client.post(
|
|
65
|
+
url=Endpoint.ROTATE_COOKIES,
|
|
66
|
+
headers=Headers.ROTATE_COOKIES.value,
|
|
67
|
+
cookies=cookies,
|
|
68
|
+
content='[000,"-0000000000000000000"]',
|
|
69
|
+
)
|
|
70
|
+
if response.status_code == 401:
|
|
71
|
+
raise AuthError
|
|
72
|
+
response.raise_for_status()
|
|
73
|
+
|
|
74
|
+
if new_1psidts := response.cookies.get("__Secure-1PSIDTS"):
|
|
75
|
+
path.write_text(new_1psidts)
|
|
76
|
+
return new_1psidts, response.cookies
|
|
77
|
+
|
|
78
|
+
return None, response.cookies
|
|
@@ -1,22 +1,38 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import random
|
|
1
3
|
from pathlib import Path
|
|
2
4
|
|
|
3
5
|
from httpx import AsyncClient
|
|
4
|
-
from pydantic import validate_call
|
|
6
|
+
from pydantic import ConfigDict, validate_call
|
|
5
7
|
|
|
6
8
|
from ..constants import Endpoint, Headers
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
def _generate_random_name(extension: str = ".txt") -> str:
|
|
12
|
+
"""
|
|
13
|
+
Generate a random filename using a large integer for better performance.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
return f"input_{random.randint(1000000, 9999999)}{extension}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
20
|
+
async def upload_file(
|
|
21
|
+
file: str | Path | bytes | io.BytesIO,
|
|
22
|
+
proxy: str | None = None,
|
|
23
|
+
filename: str | None = None,
|
|
24
|
+
) -> str:
|
|
11
25
|
"""
|
|
12
26
|
Upload a file to Google's server and return its identifier.
|
|
13
27
|
|
|
14
28
|
Parameters
|
|
15
29
|
----------
|
|
16
|
-
file : `str` | `Path`
|
|
17
|
-
Path to the file to be uploaded.
|
|
30
|
+
file : `str` | `Path` | `bytes` | `io.BytesIO`
|
|
31
|
+
Path to the file or file content to be uploaded.
|
|
18
32
|
proxy: `str`, optional
|
|
19
33
|
Proxy URL.
|
|
34
|
+
filename: `str`, optional
|
|
35
|
+
Name of the file to be uploaded. Required if file is bytes or BytesIO.
|
|
20
36
|
|
|
21
37
|
Returns
|
|
22
38
|
-------
|
|
@@ -30,28 +46,43 @@ async def upload_file(file: str | Path, proxy: str | None = None) -> str:
|
|
|
30
46
|
If the upload request failed.
|
|
31
47
|
"""
|
|
32
48
|
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
if isinstance(file, (str, Path)):
|
|
50
|
+
file_path = Path(file)
|
|
51
|
+
if not file_path.is_file():
|
|
52
|
+
raise ValueError(f"{file_path} is not a valid file.")
|
|
53
|
+
if not filename:
|
|
54
|
+
filename = file_path.name
|
|
55
|
+
file_content = file_path.read_bytes()
|
|
56
|
+
elif isinstance(file, io.BytesIO):
|
|
57
|
+
file_content = file.getvalue()
|
|
58
|
+
if not filename:
|
|
59
|
+
filename = _generate_random_name()
|
|
60
|
+
elif isinstance(file, bytes):
|
|
61
|
+
file_content = file
|
|
62
|
+
if not filename:
|
|
63
|
+
filename = _generate_random_name()
|
|
64
|
+
else:
|
|
65
|
+
raise ValueError(f"Unsupported file type: {type(file)}")
|
|
35
66
|
|
|
36
|
-
async with AsyncClient(proxy=proxy) as client:
|
|
67
|
+
async with AsyncClient(http2=True, proxy=proxy) as client:
|
|
37
68
|
response = await client.post(
|
|
38
|
-
url=Endpoint.UPLOAD
|
|
69
|
+
url=Endpoint.UPLOAD,
|
|
39
70
|
headers=Headers.UPLOAD.value,
|
|
40
|
-
files={"file":
|
|
71
|
+
files={"file": (filename, file_content)},
|
|
41
72
|
follow_redirects=True,
|
|
42
73
|
)
|
|
43
74
|
response.raise_for_status()
|
|
44
75
|
return response.text
|
|
45
76
|
|
|
46
77
|
|
|
47
|
-
def parse_file_name(file: str | Path) -> str:
|
|
78
|
+
def parse_file_name(file: str | Path | bytes | io.BytesIO) -> str:
|
|
48
79
|
"""
|
|
49
|
-
Parse the file name from the given path.
|
|
80
|
+
Parse the file name from the given path or generate a random one for in-memory data.
|
|
50
81
|
|
|
51
82
|
Parameters
|
|
52
83
|
----------
|
|
53
|
-
file : `str` | `Path`
|
|
54
|
-
Path to the file.
|
|
84
|
+
file : `str` | `Path` | `bytes` | `io.BytesIO`
|
|
85
|
+
Path to the file or file content.
|
|
55
86
|
|
|
56
87
|
Returns
|
|
57
88
|
-------
|
|
@@ -59,8 +90,10 @@ def parse_file_name(file: str | Path) -> str:
|
|
|
59
90
|
File name with extension.
|
|
60
91
|
"""
|
|
61
92
|
|
|
62
|
-
file
|
|
63
|
-
|
|
64
|
-
|
|
93
|
+
if isinstance(file, (str, Path)):
|
|
94
|
+
file = Path(file)
|
|
95
|
+
if not file.is_file():
|
|
96
|
+
raise ValueError(f"{file} is not a valid file.")
|
|
97
|
+
return file.name
|
|
65
98
|
|
|
66
|
-
return
|
|
99
|
+
return _generate_random_name()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gemini-webapi
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.18.1
|
|
4
4
|
Summary: ✨ An elegant async Python wrapper for Google Gemini web app
|
|
5
5
|
Author: UZQueen
|
|
6
6
|
License: GNU AFFERO GENERAL PUBLIC LICENSE
|
|
@@ -676,10 +676,10 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
676
676
|
Requires-Python: >=3.10
|
|
677
677
|
Description-Content-Type: text/markdown
|
|
678
678
|
License-File: LICENSE
|
|
679
|
-
Requires-Dist: httpx~=0.28.1
|
|
679
|
+
Requires-Dist: httpx[http2]~=0.28.1
|
|
680
680
|
Requires-Dist: loguru~=0.7.3
|
|
681
|
-
Requires-Dist: orjson~=3.11.
|
|
682
|
-
Requires-Dist: pydantic~=2.12.
|
|
681
|
+
Requires-Dist: orjson~=3.11.7
|
|
682
|
+
Requires-Dist: pydantic~=2.12.5
|
|
683
683
|
Dynamic: license-file
|
|
684
684
|
|
|
685
685
|
<p align="center">
|
|
@@ -717,6 +717,7 @@ A reverse-engineered asynchronous python wrapper for [Google Gemini](https://gem
|
|
|
717
717
|
- **System Prompt** - Supports customizing model's system prompt with [Gemini Gems](https://gemini.google.com/gems/view).
|
|
718
718
|
- **Extension Support** - Supports generating contents with [Gemini extensions](https://gemini.google.com/extensions) on, like YouTube and Gmail.
|
|
719
719
|
- **Classified Outputs** - Categorizes texts, thoughts, web images and AI generated images in the response.
|
|
720
|
+
- **Streaming Mode** - Supports stream generation, yielding partial outputs as they are generated.
|
|
720
721
|
- **Official Flavor** - Provides a simple and elegant interface inspired by [Google Generative AI](https://ai.google.dev/tutorials/python_quickstart)'s official API.
|
|
721
722
|
- **Asynchronous** - Utilizes `asyncio` to run generating tasks and return outputs efficiently.
|
|
722
723
|
|
|
@@ -732,6 +733,8 @@ A reverse-engineered asynchronous python wrapper for [Google Gemini](https://gem
|
|
|
732
733
|
- [Generate contents with files](#generate-contents-with-files)
|
|
733
734
|
- [Conversations across multiple turns](#conversations-across-multiple-turns)
|
|
734
735
|
- [Continue previous conversations](#continue-previous-conversations)
|
|
736
|
+
- [Delete previous conversations from Gemini history](#delete-previous-conversations-from-gemini-history)
|
|
737
|
+
- [Streaming mode](#streaming-mode)
|
|
735
738
|
- [Select language model](#select-language-model)
|
|
736
739
|
- [Apply system prompt with Gemini Gems](#apply-system-prompt-with-gemini-gems)
|
|
737
740
|
- [Manage Custom Gems](#manage-custom-gems)
|
|
@@ -901,6 +904,45 @@ async def main():
|
|
|
901
904
|
asyncio.run(main())
|
|
902
905
|
```
|
|
903
906
|
|
|
907
|
+
### Delete previous conversations from Gemini history
|
|
908
|
+
|
|
909
|
+
You can delete a specific chat from Gemini history on the server by calling `GeminiClient.delete_chat` with the chat id.
|
|
910
|
+
|
|
911
|
+
```python
|
|
912
|
+
async def main():
|
|
913
|
+
# Start a new chat session
|
|
914
|
+
chat = client.start_chat()
|
|
915
|
+
await chat.send_message("This is a temporary conversation.")
|
|
916
|
+
|
|
917
|
+
# Delete the chat
|
|
918
|
+
await client.delete_chat(chat.cid)
|
|
919
|
+
print(f"Chat deleted: {chat.cid}")
|
|
920
|
+
|
|
921
|
+
asyncio.run(main())
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
### Streaming mode
|
|
925
|
+
|
|
926
|
+
For longer responses, you can use streaming mode to receive partial outputs as they are generated. This provides a more responsive user experience, especially for real-time applications like chatbots.
|
|
927
|
+
|
|
928
|
+
The `generate_content_stream` method yields `ModelOutput` objects where the `text_delta` attribute contains only the **new characters** received since the last yield, making it easy to display incremental updates.
|
|
929
|
+
|
|
930
|
+
```python
|
|
931
|
+
async def main():
|
|
932
|
+
async for chunk in client.generate_content_stream(
|
|
933
|
+
"What's the difference between 'await' and 'async for'?"
|
|
934
|
+
):
|
|
935
|
+
print(chunk.text_delta, end="", flush=True)
|
|
936
|
+
|
|
937
|
+
print()
|
|
938
|
+
|
|
939
|
+
asyncio.run(main())
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
> [!TIP]
|
|
943
|
+
>
|
|
944
|
+
> You can also use streaming mode in multi-turn conversations with `ChatSession.send_message_stream`.
|
|
945
|
+
|
|
904
946
|
### Select language model
|
|
905
947
|
|
|
906
948
|
You can specify which language model to use by passing `model` argument to `GeminiClient.generate_content` or `GeminiClient.start_chat`. The default value is `unspecified`.
|
|
@@ -909,8 +951,8 @@ Currently available models (as of November 20, 2025):
|
|
|
909
951
|
|
|
910
952
|
- `unspecified` - Default model
|
|
911
953
|
- `gemini-3.0-pro` - Gemini 3.0 Pro
|
|
912
|
-
- `gemini-
|
|
913
|
-
- `gemini-
|
|
954
|
+
- `gemini-3.0-flash` - Gemini 3.0 Flash
|
|
955
|
+
- `gemini-3.0-flash-thinking` - Gemini 3.0 Flash Thinking
|
|
914
956
|
|
|
915
957
|
```python
|
|
916
958
|
from gemini_webapi.constants import Model
|
|
@@ -918,9 +960,9 @@ from gemini_webapi.constants import Model
|
|
|
918
960
|
async def main():
|
|
919
961
|
response1 = await client.generate_content(
|
|
920
962
|
"What's you language model version? Reply version number only.",
|
|
921
|
-
model=Model.
|
|
963
|
+
model=Model.G_3_0_FLASH,
|
|
922
964
|
)
|
|
923
|
-
print(f"Model version ({Model.
|
|
965
|
+
print(f"Model version ({Model.G_3_0_FLASH.model_name}): {response1.text}")
|
|
924
966
|
|
|
925
967
|
chat = client.start_chat(model="gemini-2.5-pro")
|
|
926
968
|
response2 = await chat.send_message("What's you language model version? Reply version number only.")
|
|
@@ -957,7 +999,7 @@ System prompt can be applied to conversations via [Gemini Gems](https://gemini.g
|
|
|
957
999
|
```python
|
|
958
1000
|
async def main():
|
|
959
1001
|
# Fetch all gems for the current account, including both predefined and user-created ones
|
|
960
|
-
await client.fetch_gems(include_hidden=False)
|
|
1002
|
+
await client.fetch_gems(include_hidden=False, language="en")
|
|
961
1003
|
|
|
962
1004
|
# Once fetched, gems will be cached in `GeminiClient.gems`
|
|
963
1005
|
gems = client.gems
|
|
@@ -968,7 +1010,7 @@ async def main():
|
|
|
968
1010
|
|
|
969
1011
|
response1 = await client.generate_content(
|
|
970
1012
|
"what's your system prompt?",
|
|
971
|
-
model=Model.
|
|
1013
|
+
model=Model.G_3_0_FLASH,
|
|
972
1014
|
gem=coding_partner,
|
|
973
1015
|
)
|
|
974
1016
|
print(response1.text)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
gemini_webapi/__init__.py,sha256=7ELCiUoI10ea3daeJxnv0UwqLVKpM7rxsgOZsPMstO8,150
|
|
2
|
+
gemini_webapi/client.py,sha256=qD6XwOkrzk1FXmUdcaq_-xoJjVpVkxAuXDy2-7vCf4A,42123
|
|
3
|
+
gemini_webapi/constants.py,sha256=VluH20wYib_9z1LPTFHQvv__mBUTNudVcb6YJWMrGt0,3546
|
|
4
|
+
gemini_webapi/exceptions.py,sha256=qkXrIpr0L7LtGbq3VcTO8D1xZ50pJtt0dDRp5I3uDSg,1038
|
|
5
|
+
gemini_webapi/components/__init__.py,sha256=wolxuAJJ32-jmHOKgpsesexP7hXea1JMo5vI52wysTI,48
|
|
6
|
+
gemini_webapi/components/gem_mixin.py,sha256=cSyxDyGmr-O2WK9XF1I74l3uIDGAKG4M2EOukDBkvS4,9763
|
|
7
|
+
gemini_webapi/types/__init__.py,sha256=1DU4JEw2KHQJbtOgOuvoEXtzQCMwAkyo0koSFVdT9JY,192
|
|
8
|
+
gemini_webapi/types/candidate.py,sha256=5Gy2UiOgwlsrFZ7wQsuHyS2jmKsgQLGkSHbqbPTEnqg,1549
|
|
9
|
+
gemini_webapi/types/gem.py,sha256=3Ppjq9V22Zp4Lb9a9ZnDviDKQpfSQf8UZxqOEjeEWd4,4070
|
|
10
|
+
gemini_webapi/types/grpc.py,sha256=S64h1oeC7ZJC50kmS_C2CQ7WVTanhJ4kqTFx5ZYayXI,917
|
|
11
|
+
gemini_webapi/types/image.py,sha256=DYHSSwKxd8-ipaeFeyHuHt95eh1D681irCq0oIqspYA,6240
|
|
12
|
+
gemini_webapi/types/modeloutput.py,sha256=L-qI1a9sPTFFNbxBgDm6FKTKjMJQxiRxNLthr7owUy8,1422
|
|
13
|
+
gemini_webapi/utils/__init__.py,sha256=w_OwRm9gD8j2DP7ReomhGWx3sbnPGyRC-8DXt5DZXhc,376
|
|
14
|
+
gemini_webapi/utils/decorators.py,sha256=iTTVIgG1fxOVMluy9fKeVrTmj4_K1h1q4lVLCjFaLhc,3462
|
|
15
|
+
gemini_webapi/utils/get_access_token.py,sha256=6EcLhcpS8_-hxodBvQwu76PECVBnQQtzt9AYqSi0ps8,8162
|
|
16
|
+
gemini_webapi/utils/load_browser_cookies.py,sha256=OHCfe27DpV_rloIDgW9Xpeb0mkfzbYONNiholw0ElXU,1791
|
|
17
|
+
gemini_webapi/utils/logger.py,sha256=0VcxhVLhHBRDQutNCpapP1y_MhPoQ2ud1uIFLqxC3Z8,958
|
|
18
|
+
gemini_webapi/utils/parsing.py,sha256=iNrczcArdaCQ9_xD0isf4LDEwSTbBjq_frgvMdqm1Tk,7291
|
|
19
|
+
gemini_webapi/utils/rotate_1psidts.py,sha256=7W_D5NGJ9_dpF0EgVaU_ykL1ebvQK7P28bQN2HVlX5M,2359
|
|
20
|
+
gemini_webapi/utils/upload_file.py,sha256=TiLnXdNvY8NRFZih-wp_BpDA6LxAVQDQakzmq2Ur9Ak,2781
|
|
21
|
+
gemini_webapi-1.18.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
22
|
+
gemini_webapi-1.18.1.dist-info/METADATA,sha256=y4Kg_-Dn4PZDk2xtBmjtBAUnDI6fqHb0F-NHAQ91sTk,63301
|
|
23
|
+
gemini_webapi-1.18.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
24
|
+
gemini_webapi-1.18.1.dist-info/top_level.txt,sha256=dtWtug_ZrmnUqCYuu8NmGzTgWglHeNzhHU_hXmqZGWE,14
|
|
25
|
+
gemini_webapi-1.18.1.dist-info/RECORD,,
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
gemini_webapi/__init__.py,sha256=7ELCiUoI10ea3daeJxnv0UwqLVKpM7rxsgOZsPMstO8,150
|
|
2
|
-
gemini_webapi/client.py,sha256=OJSScOnl6rUbDOJ7hz-jn_1u13_HdjczPJsstOgNTGw,29254
|
|
3
|
-
gemini_webapi/constants.py,sha256=-UHHwSoY4pzEfRTGpfb8UMgCNVbgSc3RLSOZpVOyNpM,3409
|
|
4
|
-
gemini_webapi/exceptions.py,sha256=qkXrIpr0L7LtGbq3VcTO8D1xZ50pJtt0dDRp5I3uDSg,1038
|
|
5
|
-
gemini_webapi/components/__init__.py,sha256=wolxuAJJ32-jmHOKgpsesexP7hXea1JMo5vI52wysTI,48
|
|
6
|
-
gemini_webapi/components/gem_mixin.py,sha256=WPJkYDS4yQpLMBNQ94LQo5w59RgkllWaSiHsFG1k5GU,8795
|
|
7
|
-
gemini_webapi/types/__init__.py,sha256=1DU4JEw2KHQJbtOgOuvoEXtzQCMwAkyo0koSFVdT9JY,192
|
|
8
|
-
gemini_webapi/types/candidate.py,sha256=67BhY75toE5mVuB21cmHcTFtw332V_KmCjr3-9VTbJo,1477
|
|
9
|
-
gemini_webapi/types/gem.py,sha256=3Ppjq9V22Zp4Lb9a9ZnDviDKQpfSQf8UZxqOEjeEWd4,4070
|
|
10
|
-
gemini_webapi/types/grpc.py,sha256=S64h1oeC7ZJC50kmS_C2CQ7WVTanhJ4kqTFx5ZYayXI,917
|
|
11
|
-
gemini_webapi/types/image.py,sha256=9miYAu3htBjNScCWfBJ49b_RtWDW9XFWO1NRJDvLgM8,6173
|
|
12
|
-
gemini_webapi/types/modeloutput.py,sha256=h07kQOkL5r-oPLvZ59uVtO1eP4FGy5ZpzuYQzAeQdr8,1196
|
|
13
|
-
gemini_webapi/utils/__init__.py,sha256=k8hV2zn6tD_BEpd1Xya6ED0deijsmzb1e9XxdFhJzIE,418
|
|
14
|
-
gemini_webapi/utils/decorators.py,sha256=uzIXoZOC0_Om19bbVXf_nw2w2NhI2qJL1o45FU6o6fI,1780
|
|
15
|
-
gemini_webapi/utils/get_access_token.py,sha256=twZtTtvOnGxHhikCOj1HGErj2r5dT_BNcvqv5A0dtXg,7194
|
|
16
|
-
gemini_webapi/utils/load_browser_cookies.py,sha256=OHCfe27DpV_rloIDgW9Xpeb0mkfzbYONNiholw0ElXU,1791
|
|
17
|
-
gemini_webapi/utils/logger.py,sha256=0VcxhVLhHBRDQutNCpapP1y_MhPoQ2ud1uIFLqxC3Z8,958
|
|
18
|
-
gemini_webapi/utils/parsing.py,sha256=wDQsK1RcfrPr7c6w1JpxoPjLtkFIkcPmTX8jjfleJf0,2080
|
|
19
|
-
gemini_webapi/utils/rotate_1psidts.py,sha256=SIqkqjZrEAyXB38qTEY9apxHVwczpFut4I0A77eWIFk,1790
|
|
20
|
-
gemini_webapi/utils/upload_file.py,sha256=z5p03l6oQP74wzF0MF-Kbnav5k1nb7uYyzx-f8pxSVI,1456
|
|
21
|
-
gemini_webapi-1.17.3.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
22
|
-
gemini_webapi-1.17.3.dist-info/METADATA,sha256=Ld5hDcwuox-uqslhP83-gzyYVEZa7wgOs5hX2NcG7Y4,61756
|
|
23
|
-
gemini_webapi-1.17.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
-
gemini_webapi-1.17.3.dist-info/top_level.txt,sha256=dtWtug_ZrmnUqCYuu8NmGzTgWglHeNzhHU_hXmqZGWE,14
|
|
25
|
-
gemini_webapi-1.17.3.dist-info/RECORD,,
|
|
File without changes
|