gemini-webapi 1.14.2__tar.gz → 1.14.4__tar.gz
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-1.14.2 → gemini_webapi-1.14.4}/.github/workflows/pypi-publish.yml +1 -1
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/PKG-INFO +7 -3
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/README.md +5 -1
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/pyproject.toml +1 -1
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/client.py +76 -178
- gemini_webapi-1.14.4/src/gemini_webapi/components/__init__.py +3 -0
- gemini_webapi-1.14.4/src/gemini_webapi/components/gem_mixin.py +151 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/constants.py +13 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/types/__init__.py +1 -0
- gemini_webapi-1.14.4/src/gemini_webapi/types/grpc.py +34 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/types/image.py +4 -4
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/utils/__init__.py +1 -0
- gemini_webapi-1.14.4/src/gemini_webapi/utils/decorators.py +52 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi.egg-info/PKG-INFO +7 -3
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi.egg-info/SOURCES.txt +5 -1
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi.egg-info/requires.txt +1 -1
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/tests/test_client_features.py +11 -10
- gemini_webapi-1.14.4/tests/test_gem_mixin.py +33 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/tests/test_save_image.py +11 -4
- gemini_webapi-1.14.2/tests/test_rotate_cookies.py +0 -24
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/.github/dependabot.yml +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/.github/workflows/github-release.yml +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/.gitignore +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/.vscode/launch.json +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/.vscode/settings.json +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/LICENSE +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/assets/banner.png +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/assets/favicon.png +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/assets/logo.svg +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/assets/sample.pdf +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/setup.cfg +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/__init__.py +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/exceptions.py +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/types/candidate.py +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/types/gem.py +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/types/modeloutput.py +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/utils/get_access_token.py +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/utils/load_browser_cookies.py +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/utils/logger.py +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/utils/rotate_1psidts.py +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/utils/upload_file.py +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi.egg-info/dependency_links.txt +0 -0
- {gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gemini-webapi
|
|
3
|
-
Version: 1.14.
|
|
3
|
+
Version: 1.14.4
|
|
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
|
|
@@ -678,7 +678,7 @@ Description-Content-Type: text/markdown
|
|
|
678
678
|
License-File: LICENSE
|
|
679
679
|
Requires-Dist: httpx[http2]~=0.28.1
|
|
680
680
|
Requires-Dist: loguru~=0.7.3
|
|
681
|
-
Requires-Dist: orjson~=3.
|
|
681
|
+
Requires-Dist: orjson~=3.11.1
|
|
682
682
|
Requires-Dist: pydantic~=2.11.5
|
|
683
683
|
Dynamic: license-file
|
|
684
684
|
|
|
@@ -931,10 +931,14 @@ asyncio.run(main())
|
|
|
931
931
|
|
|
932
932
|
System prompt can be applied to conversations via [Gemini Gems](https://gemini.google.com/gems/view). To use a gem, you can pass `gem` argument to `GeminiClient.generate_content` or `GeminiClient.start_chat`. `gem` can be either a string of gem id or a `gemini_webapi.Gem` object. Only one gem can be applied to a single conversation.
|
|
933
933
|
|
|
934
|
+
> [!TIP]
|
|
935
|
+
>
|
|
936
|
+
> There are some system predefined gems that by default are not shown to users (and therefore may not work properly). Use `client.fetch_gems(include_hidden=True)` to include them in the fetch result.
|
|
937
|
+
|
|
934
938
|
```python
|
|
935
939
|
async def main():
|
|
936
940
|
# Fetch all gems for the current account, including both predefined and user-created ones
|
|
937
|
-
await client.fetch_gems()
|
|
941
|
+
await client.fetch_gems(include_hidden=False)
|
|
938
942
|
|
|
939
943
|
# Once fetched, gems will be cached in `GeminiClient.gems`
|
|
940
944
|
gems = client.gems
|
|
@@ -247,10 +247,14 @@ asyncio.run(main())
|
|
|
247
247
|
|
|
248
248
|
System prompt can be applied to conversations via [Gemini Gems](https://gemini.google.com/gems/view). To use a gem, you can pass `gem` argument to `GeminiClient.generate_content` or `GeminiClient.start_chat`. `gem` can be either a string of gem id or a `gemini_webapi.Gem` object. Only one gem can be applied to a single conversation.
|
|
249
249
|
|
|
250
|
+
> [!TIP]
|
|
251
|
+
>
|
|
252
|
+
> There are some system predefined gems that by default are not shown to users (and therefore may not work properly). Use `client.fetch_gems(include_hidden=True)` to include them in the fetch result.
|
|
253
|
+
|
|
250
254
|
```python
|
|
251
255
|
async def main():
|
|
252
256
|
# Fetch all gems for the current account, including both predefined and user-created ones
|
|
253
|
-
await client.fetch_gems()
|
|
257
|
+
await client.fetch_gems(include_hidden=False)
|
|
254
258
|
|
|
255
259
|
# Once fetched, gems will be cached in `GeminiClient.gems`
|
|
256
260
|
gems = client.gems
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import functools
|
|
3
|
-
import itertools
|
|
4
2
|
import re
|
|
5
3
|
from asyncio import Task
|
|
6
4
|
from pathlib import Path
|
|
7
5
|
from typing import Any, Optional
|
|
8
6
|
|
|
9
7
|
import orjson as json
|
|
10
|
-
from httpx import AsyncClient, ReadTimeout
|
|
8
|
+
from httpx import AsyncClient, ReadTimeout, Response
|
|
11
9
|
|
|
10
|
+
from .components import GemMixin
|
|
12
11
|
from .constants import Endpoint, ErrorCode, Headers, Model
|
|
13
12
|
from .exceptions import (
|
|
14
13
|
AuthError,
|
|
@@ -20,67 +19,27 @@ from .exceptions import (
|
|
|
20
19
|
ModelInvalid,
|
|
21
20
|
TemporarilyBlocked,
|
|
22
21
|
)
|
|
23
|
-
from .types import
|
|
22
|
+
from .types import (
|
|
23
|
+
WebImage,
|
|
24
|
+
GeneratedImage,
|
|
25
|
+
Candidate,
|
|
26
|
+
ModelOutput,
|
|
27
|
+
Gem,
|
|
28
|
+
RPCData,
|
|
29
|
+
)
|
|
24
30
|
from .utils import (
|
|
25
31
|
upload_file,
|
|
26
32
|
parse_file_name,
|
|
27
33
|
rotate_1psidts,
|
|
28
34
|
get_access_token,
|
|
29
35
|
load_browser_cookies,
|
|
36
|
+
running,
|
|
30
37
|
rotate_tasks,
|
|
31
38
|
logger,
|
|
32
39
|
)
|
|
33
40
|
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
"""
|
|
37
|
-
Decorator to check if client is running before making a request.
|
|
38
|
-
|
|
39
|
-
Parameters
|
|
40
|
-
----------
|
|
41
|
-
retry: `int`, optional
|
|
42
|
-
Max number of retries when `gemini_webapi.APIError` is raised.
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
def decorator(func):
|
|
46
|
-
@functools.wraps(func)
|
|
47
|
-
async def wrapper(client: "GeminiClient", *args, retry=retry, **kwargs):
|
|
48
|
-
try:
|
|
49
|
-
if not client.running:
|
|
50
|
-
await client.init(
|
|
51
|
-
timeout=client.timeout,
|
|
52
|
-
auto_close=client.auto_close,
|
|
53
|
-
close_delay=client.close_delay,
|
|
54
|
-
auto_refresh=client.auto_refresh,
|
|
55
|
-
refresh_interval=client.refresh_interval,
|
|
56
|
-
verbose=False,
|
|
57
|
-
)
|
|
58
|
-
if client.running:
|
|
59
|
-
return await func(client, *args, **kwargs)
|
|
60
|
-
|
|
61
|
-
# Should not reach here
|
|
62
|
-
raise APIError(
|
|
63
|
-
f"Invalid function call: GeminiClient.{func.__name__}. Client initialization failed."
|
|
64
|
-
)
|
|
65
|
-
else:
|
|
66
|
-
return await func(client, *args, **kwargs)
|
|
67
|
-
except APIError as e:
|
|
68
|
-
# Image generation takes too long, only retry once
|
|
69
|
-
if isinstance(e, ImageGenerationError):
|
|
70
|
-
retry = min(1, retry)
|
|
71
|
-
|
|
72
|
-
if retry > 0:
|
|
73
|
-
await asyncio.sleep(1)
|
|
74
|
-
return await wrapper(client, *args, retry=retry - 1, **kwargs)
|
|
75
|
-
|
|
76
|
-
raise
|
|
77
|
-
|
|
78
|
-
return wrapper
|
|
79
|
-
|
|
80
|
-
return decorator
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class GeminiClient:
|
|
42
|
+
class GeminiClient(GemMixin):
|
|
84
43
|
"""
|
|
85
44
|
Async httpx client interface for gemini.google.com.
|
|
86
45
|
|
|
@@ -117,7 +76,7 @@ class GeminiClient:
|
|
|
117
76
|
"close_task",
|
|
118
77
|
"auto_refresh",
|
|
119
78
|
"refresh_interval",
|
|
120
|
-
"_gems",
|
|
79
|
+
"_gems", # From GemMixin
|
|
121
80
|
"kwargs",
|
|
122
81
|
]
|
|
123
82
|
|
|
@@ -128,6 +87,7 @@ class GeminiClient:
|
|
|
128
87
|
proxy: str | None = None,
|
|
129
88
|
**kwargs,
|
|
130
89
|
):
|
|
90
|
+
super().__init__()
|
|
131
91
|
self.cookies = {}
|
|
132
92
|
self.proxy = proxy
|
|
133
93
|
self.running: bool = False
|
|
@@ -139,7 +99,6 @@ class GeminiClient:
|
|
|
139
99
|
self.close_task: Task | None = None
|
|
140
100
|
self.auto_refresh: bool = True
|
|
141
101
|
self.refresh_interval: float = 540
|
|
142
|
-
self._gems: GemJar | None = None
|
|
143
102
|
self.kwargs = kwargs
|
|
144
103
|
|
|
145
104
|
# Validate cookies
|
|
@@ -277,125 +236,6 @@ class GeminiClient:
|
|
|
277
236
|
self.cookies["__Secure-1PSIDTS"] = new_1psidts
|
|
278
237
|
await asyncio.sleep(self.refresh_interval)
|
|
279
238
|
|
|
280
|
-
@property
|
|
281
|
-
def gems(self) -> GemJar:
|
|
282
|
-
"""
|
|
283
|
-
Returns a `GemJar` object containing cached gems.
|
|
284
|
-
Only available after calling `GeminiClient.fetch_gems()`.
|
|
285
|
-
|
|
286
|
-
Returns
|
|
287
|
-
-------
|
|
288
|
-
:class:`GemJar`
|
|
289
|
-
Refer to `gemini_webapi.types.GemJar`.
|
|
290
|
-
|
|
291
|
-
Raises
|
|
292
|
-
------
|
|
293
|
-
`RuntimeError`
|
|
294
|
-
If `GeminiClient.fetch_gems()` has not been called before accessing this property.
|
|
295
|
-
"""
|
|
296
|
-
|
|
297
|
-
if self._gems is None:
|
|
298
|
-
raise RuntimeError(
|
|
299
|
-
"Gems not fetched yet. Call `GeminiClient.fetch_gems()` method to fetch gems from gemini.google.com."
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
return self._gems
|
|
303
|
-
|
|
304
|
-
@running(retry=2)
|
|
305
|
-
async def fetch_gems(self, **kwargs) -> GemJar:
|
|
306
|
-
"""
|
|
307
|
-
Get a list of available gems from gemini, including system predefined gems and user-created custom gems.
|
|
308
|
-
|
|
309
|
-
Note that network request will be sent every time this method is called.
|
|
310
|
-
Once the gems are fetched, they will be cached and accessible via `GeminiClient.gems` property.
|
|
311
|
-
|
|
312
|
-
Returns
|
|
313
|
-
-------
|
|
314
|
-
:class:`GemJar`
|
|
315
|
-
Refer to `gemini_webapi.types.GemJar`.
|
|
316
|
-
"""
|
|
317
|
-
|
|
318
|
-
try:
|
|
319
|
-
response = await self.client.post(
|
|
320
|
-
Endpoint.BATCH_EXEC,
|
|
321
|
-
data={
|
|
322
|
-
"at": self.access_token,
|
|
323
|
-
"f.req": json.dumps(
|
|
324
|
-
[
|
|
325
|
-
[
|
|
326
|
-
["CNgdBe", '[2,["en"],0]', None, "custom"],
|
|
327
|
-
["CNgdBe", '[3,["en"],0]', None, "system"],
|
|
328
|
-
]
|
|
329
|
-
]
|
|
330
|
-
).decode(),
|
|
331
|
-
},
|
|
332
|
-
**kwargs,
|
|
333
|
-
)
|
|
334
|
-
except ReadTimeout:
|
|
335
|
-
raise TimeoutError(
|
|
336
|
-
"Fetch gems request timed out, please try again. If the problem persists, "
|
|
337
|
-
"consider setting a higher `timeout` value when initializing GeminiClient."
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
if response.status_code != 200:
|
|
341
|
-
raise APIError(
|
|
342
|
-
f"Failed to fetch gems. Request failed with status code {response.status_code}"
|
|
343
|
-
)
|
|
344
|
-
else:
|
|
345
|
-
try:
|
|
346
|
-
response_json = json.loads(response.text.split("\n")[2])
|
|
347
|
-
|
|
348
|
-
predefined_gems, custom_gems = [], []
|
|
349
|
-
|
|
350
|
-
for part in response_json:
|
|
351
|
-
if part[-1] == "system":
|
|
352
|
-
predefined_gems = json.loads(part[2])[2]
|
|
353
|
-
elif part[-1] == "custom":
|
|
354
|
-
if custom_gems_container := json.loads(part[2]):
|
|
355
|
-
custom_gems = custom_gems_container[2]
|
|
356
|
-
|
|
357
|
-
if not predefined_gems and not custom_gems:
|
|
358
|
-
raise Exception
|
|
359
|
-
except Exception:
|
|
360
|
-
await self.close()
|
|
361
|
-
logger.debug(f"Invalid response: {response.text}")
|
|
362
|
-
raise APIError(
|
|
363
|
-
"Failed to fetch gems. Invalid response data received. Client will try to re-initialize on next request."
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
self._gems = GemJar(
|
|
367
|
-
itertools.chain(
|
|
368
|
-
(
|
|
369
|
-
(
|
|
370
|
-
gem[0],
|
|
371
|
-
Gem(
|
|
372
|
-
id=gem[0],
|
|
373
|
-
name=gem[1][0],
|
|
374
|
-
description=gem[1][1],
|
|
375
|
-
prompt=gem[2] and gem[2][0] or None,
|
|
376
|
-
predefined=True,
|
|
377
|
-
),
|
|
378
|
-
)
|
|
379
|
-
for gem in predefined_gems
|
|
380
|
-
),
|
|
381
|
-
(
|
|
382
|
-
(
|
|
383
|
-
gem[0],
|
|
384
|
-
Gem(
|
|
385
|
-
id=gem[0],
|
|
386
|
-
name=gem[1][0],
|
|
387
|
-
description=gem[1][1],
|
|
388
|
-
prompt=gem[2] and gem[2][0] or None,
|
|
389
|
-
predefined=False,
|
|
390
|
-
),
|
|
391
|
-
)
|
|
392
|
-
for gem in custom_gems
|
|
393
|
-
),
|
|
394
|
-
)
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
return self._gems
|
|
398
|
-
|
|
399
239
|
@running(retry=2)
|
|
400
240
|
async def generate_content(
|
|
401
241
|
self,
|
|
@@ -609,10 +449,21 @@ class GeminiClient:
|
|
|
609
449
|
generated_images = [
|
|
610
450
|
GeneratedImage(
|
|
611
451
|
url=generated_image[0][3][3],
|
|
612
|
-
title=
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
452
|
+
title=(
|
|
453
|
+
f"[Generated Image {generated_image[3][6]}]"
|
|
454
|
+
if generated_image[3][6]
|
|
455
|
+
else "[Generated Image]"
|
|
456
|
+
),
|
|
457
|
+
alt=(
|
|
458
|
+
generated_image[3][5][image_index]
|
|
459
|
+
if generated_image[3][5]
|
|
460
|
+
and len(generated_image[3][5]) > image_index
|
|
461
|
+
else (
|
|
462
|
+
generated_image[3][5][0]
|
|
463
|
+
if generated_image[3][5]
|
|
464
|
+
else ""
|
|
465
|
+
)
|
|
466
|
+
),
|
|
616
467
|
proxy=self.proxy,
|
|
617
468
|
cookies=self.cookies,
|
|
618
469
|
)
|
|
@@ -665,6 +516,53 @@ class GeminiClient:
|
|
|
665
516
|
|
|
666
517
|
return ChatSession(geminiclient=self, **kwargs)
|
|
667
518
|
|
|
519
|
+
async def _batch_execute(self, payloads: list[RPCData], **kwargs) -> Response:
|
|
520
|
+
"""
|
|
521
|
+
Execute a batch of requests to Gemini API.
|
|
522
|
+
|
|
523
|
+
Parameters
|
|
524
|
+
----------
|
|
525
|
+
payloads: `list[GRPC]`
|
|
526
|
+
List of `gemini_webapi.types.GRPC` objects to be executed.
|
|
527
|
+
kwargs: `dict`, optional
|
|
528
|
+
Additional arguments which will be passed to the post request.
|
|
529
|
+
Refer to `httpx.AsyncClient.request` for more information.
|
|
530
|
+
|
|
531
|
+
Returns
|
|
532
|
+
-------
|
|
533
|
+
:class:`httpx.Response`
|
|
534
|
+
Response object containing the result of the batch execution.
|
|
535
|
+
"""
|
|
536
|
+
|
|
537
|
+
try:
|
|
538
|
+
response = await self.client.post(
|
|
539
|
+
Endpoint.BATCH_EXEC,
|
|
540
|
+
data={
|
|
541
|
+
"at": self.access_token,
|
|
542
|
+
"f.req": json.dumps(
|
|
543
|
+
[[payload.serialize() for payload in payloads]]
|
|
544
|
+
).decode(),
|
|
545
|
+
},
|
|
546
|
+
**kwargs,
|
|
547
|
+
)
|
|
548
|
+
except ReadTimeout:
|
|
549
|
+
raise TimeoutError(
|
|
550
|
+
"Batch execute request timed out, please try again. If the problem persists, "
|
|
551
|
+
"consider setting a higher `timeout` value when initializing GeminiClient."
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if response.status_code != 200:
|
|
555
|
+
logger.debug(
|
|
556
|
+
f"Batch execution failed with status code {response.status_code}. "
|
|
557
|
+
f"RPC: {', '.join({payload.rpcid.name for payload in payloads})}; "
|
|
558
|
+
f"Invalid response: {response.text}"
|
|
559
|
+
)
|
|
560
|
+
raise APIError(
|
|
561
|
+
f"Batch execution failed with status code {response.status_code}"
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
return response
|
|
565
|
+
|
|
668
566
|
|
|
669
567
|
class ChatSession:
|
|
670
568
|
"""
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
|
|
3
|
+
import orjson as json
|
|
4
|
+
|
|
5
|
+
from ..constants import GRPC
|
|
6
|
+
from ..exceptions import APIError
|
|
7
|
+
from ..types import Gem, GemJar, RPCData
|
|
8
|
+
from ..utils import running, logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GemMixin:
|
|
12
|
+
"""
|
|
13
|
+
Mixin class providing gem-related functionality for GeminiClient.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, *args, **kwargs):
|
|
17
|
+
super().__init__(*args, **kwargs)
|
|
18
|
+
self._gems: GemJar | None = None
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def gems(self) -> GemJar:
|
|
22
|
+
"""
|
|
23
|
+
Returns a `GemJar` object containing cached gems.
|
|
24
|
+
Only available after calling `GeminiClient.fetch_gems()`.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
:class:`GemJar`
|
|
29
|
+
Refer to `gemini_webapi.types.GemJar`.
|
|
30
|
+
|
|
31
|
+
Raises
|
|
32
|
+
------
|
|
33
|
+
`RuntimeError`
|
|
34
|
+
If `GeminiClient.fetch_gems()` has not been called before accessing this property.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
if self._gems is None:
|
|
38
|
+
raise RuntimeError(
|
|
39
|
+
"Gems not fetched yet. Call `GeminiClient.fetch_gems()` method to fetch gems from gemini.google.com."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return self._gems
|
|
43
|
+
|
|
44
|
+
@running(retry=2)
|
|
45
|
+
async def fetch_gems(self, include_hidden: bool = False, **kwargs) -> GemJar:
|
|
46
|
+
"""
|
|
47
|
+
Get a list of available gems from gemini, including system predefined gems and user-created custom gems.
|
|
48
|
+
|
|
49
|
+
Note that network request will be sent every time this method is called.
|
|
50
|
+
Once the gems are fetched, they will be cached and accessible via `GeminiClient.gems` property.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
include_hidden: `bool`, optional
|
|
55
|
+
There are some predefined gems that by default are not shown to users (and therefore may not work properly).
|
|
56
|
+
Set this parameter to `True` to include them in the fetched gem list.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
:class:`GemJar`
|
|
61
|
+
Refer to `gemini_webapi.types.GemJar`.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
response = await self._batch_execute(
|
|
65
|
+
[
|
|
66
|
+
RPCData(
|
|
67
|
+
rpcid=GRPC.LIST_GEMS,
|
|
68
|
+
payload="[4]" if include_hidden else "[3]",
|
|
69
|
+
identifier="system",
|
|
70
|
+
),
|
|
71
|
+
RPCData(
|
|
72
|
+
rpcid=GRPC.LIST_GEMS,
|
|
73
|
+
payload="[2]",
|
|
74
|
+
identifier="custom",
|
|
75
|
+
),
|
|
76
|
+
],
|
|
77
|
+
**kwargs,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
response_json = json.loads(response.text.split("\n")[2])
|
|
82
|
+
|
|
83
|
+
predefined_gems, custom_gems = [], []
|
|
84
|
+
|
|
85
|
+
for part in response_json:
|
|
86
|
+
if part[-1] == "system":
|
|
87
|
+
predefined_gems = json.loads(part[2])[2]
|
|
88
|
+
elif part[-1] == "custom":
|
|
89
|
+
if custom_gems_container := json.loads(part[2]):
|
|
90
|
+
custom_gems = custom_gems_container[2]
|
|
91
|
+
|
|
92
|
+
if not predefined_gems and not custom_gems:
|
|
93
|
+
raise Exception
|
|
94
|
+
except Exception:
|
|
95
|
+
await self.close()
|
|
96
|
+
logger.debug(f"Invalid response: {response.text}")
|
|
97
|
+
raise APIError(
|
|
98
|
+
"Failed to fetch gems. Invalid response data received. Client will try to re-initialize on next request."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self._gems = GemJar(
|
|
102
|
+
itertools.chain(
|
|
103
|
+
(
|
|
104
|
+
(
|
|
105
|
+
gem[0],
|
|
106
|
+
Gem(
|
|
107
|
+
id=gem[0],
|
|
108
|
+
name=gem[1][0],
|
|
109
|
+
description=gem[1][1],
|
|
110
|
+
prompt=gem[2] and gem[2][0] or None,
|
|
111
|
+
predefined=True,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
for gem in predefined_gems
|
|
115
|
+
),
|
|
116
|
+
(
|
|
117
|
+
(
|
|
118
|
+
gem[0],
|
|
119
|
+
Gem(
|
|
120
|
+
id=gem[0],
|
|
121
|
+
name=gem[1][0],
|
|
122
|
+
description=gem[1][1],
|
|
123
|
+
prompt=gem[2] and gem[2][0] or None,
|
|
124
|
+
predefined=False,
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
for gem in custom_gems
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return self._gems
|
|
133
|
+
|
|
134
|
+
@running(retry=2)
|
|
135
|
+
async def delete_gem(self, gem: Gem | str, **kwargs) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Delete a custom gem from gemini.google.com.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
gem: `Gem | str`
|
|
142
|
+
Gem to delete, can be either a `gemini_webapi.types.Gem` object or a gem id string.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
if isinstance(gem, Gem):
|
|
146
|
+
gem = gem.id
|
|
147
|
+
|
|
148
|
+
await self._batch_execute(
|
|
149
|
+
[RPCData(rpcid=GRPC.DELETE_GEM, payload=[gem])],
|
|
150
|
+
**kwargs,
|
|
151
|
+
)
|
|
@@ -10,6 +10,19 @@ class Endpoint(StrEnum):
|
|
|
10
10
|
BATCH_EXEC = "https://gemini.google.com/_/BardChatUi/data/batchexecute"
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class GRPC(StrEnum):
|
|
14
|
+
"""
|
|
15
|
+
Google RPC ids used in Gemini API.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# Chat methods
|
|
19
|
+
READ_CHAT = "hNvQHb"
|
|
20
|
+
|
|
21
|
+
# Gem methods
|
|
22
|
+
LIST_GEMS = "CNgdBe"
|
|
23
|
+
DELETE_GEM = "UXcSJb"
|
|
24
|
+
|
|
25
|
+
|
|
13
26
|
class Headers(Enum):
|
|
14
27
|
GEMINI = {
|
|
15
28
|
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from ..constants import GRPC
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RPCData(BaseModel):
|
|
7
|
+
"""
|
|
8
|
+
Helper class containing necessary data for Google RPC calls.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
rpcid : GRPC
|
|
13
|
+
Google RPC ID.
|
|
14
|
+
payload : str
|
|
15
|
+
Payload for the RPC call.
|
|
16
|
+
identifier : str, optional
|
|
17
|
+
Identifier/order for the RPC call, defaults to "generic".
|
|
18
|
+
Makes sense if there are multiple RPC calls in a batch, where this identifier
|
|
19
|
+
can be used to distinguish between responses.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
rpcid: GRPC
|
|
23
|
+
payload: str
|
|
24
|
+
identifier: str = "generic"
|
|
25
|
+
|
|
26
|
+
def __repr__(self):
|
|
27
|
+
return f"GRPC(rpcid='{self.rpcid}', payload='{self.payload}', identifier='{self.identifier}')"
|
|
28
|
+
|
|
29
|
+
def serialize(self) -> list:
|
|
30
|
+
"""
|
|
31
|
+
Serializes object into formatted payload ready for RPC call.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
return [self.rpcid, self.payload, None, self.identifier]
|
|
@@ -30,10 +30,10 @@ class Image(BaseModel):
|
|
|
30
30
|
proxy: str | None = None
|
|
31
31
|
|
|
32
32
|
def __str__(self):
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
return (
|
|
34
|
+
f"Image(title='{self.title}', alt='{self.alt}', "
|
|
35
|
+
f"url='{len(self.url) <= 20 and self.url or self.url[:8] + '...' + self.url[-12:]}')"
|
|
36
|
+
)
|
|
37
37
|
|
|
38
38
|
async def save(
|
|
39
39
|
self,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
|
|
4
|
+
from ..exceptions import APIError, ImageGenerationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def running(retry: int = 0) -> callable:
|
|
8
|
+
"""
|
|
9
|
+
Decorator to check if GeminiClient is running before making a request.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
retry: `int`, optional
|
|
14
|
+
Max number of retries when `gemini_webapi.APIError` is raised.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def decorator(func):
|
|
18
|
+
@functools.wraps(func)
|
|
19
|
+
async def wrapper(client, *args, retry=retry, **kwargs):
|
|
20
|
+
try:
|
|
21
|
+
if not client.running:
|
|
22
|
+
await client.init(
|
|
23
|
+
timeout=client.timeout,
|
|
24
|
+
auto_close=client.auto_close,
|
|
25
|
+
close_delay=client.close_delay,
|
|
26
|
+
auto_refresh=client.auto_refresh,
|
|
27
|
+
refresh_interval=client.refresh_interval,
|
|
28
|
+
verbose=False,
|
|
29
|
+
)
|
|
30
|
+
if client.running:
|
|
31
|
+
return await func(client, *args, **kwargs)
|
|
32
|
+
|
|
33
|
+
# Should not reach here
|
|
34
|
+
raise APIError(
|
|
35
|
+
f"Invalid function call: GeminiClient.{func.__name__}. Client initialization failed."
|
|
36
|
+
)
|
|
37
|
+
else:
|
|
38
|
+
return await func(client, *args, **kwargs)
|
|
39
|
+
except APIError as e:
|
|
40
|
+
# Image generation takes too long, only retry once
|
|
41
|
+
if isinstance(e, ImageGenerationError):
|
|
42
|
+
retry = min(1, retry)
|
|
43
|
+
|
|
44
|
+
if retry > 0:
|
|
45
|
+
await asyncio.sleep(1)
|
|
46
|
+
return await wrapper(client, *args, retry=retry - 1, **kwargs)
|
|
47
|
+
|
|
48
|
+
raise
|
|
49
|
+
|
|
50
|
+
return wrapper
|
|
51
|
+
|
|
52
|
+
return decorator
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gemini-webapi
|
|
3
|
-
Version: 1.14.
|
|
3
|
+
Version: 1.14.4
|
|
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
|
|
@@ -678,7 +678,7 @@ Description-Content-Type: text/markdown
|
|
|
678
678
|
License-File: LICENSE
|
|
679
679
|
Requires-Dist: httpx[http2]~=0.28.1
|
|
680
680
|
Requires-Dist: loguru~=0.7.3
|
|
681
|
-
Requires-Dist: orjson~=3.
|
|
681
|
+
Requires-Dist: orjson~=3.11.1
|
|
682
682
|
Requires-Dist: pydantic~=2.11.5
|
|
683
683
|
Dynamic: license-file
|
|
684
684
|
|
|
@@ -931,10 +931,14 @@ asyncio.run(main())
|
|
|
931
931
|
|
|
932
932
|
System prompt can be applied to conversations via [Gemini Gems](https://gemini.google.com/gems/view). To use a gem, you can pass `gem` argument to `GeminiClient.generate_content` or `GeminiClient.start_chat`. `gem` can be either a string of gem id or a `gemini_webapi.Gem` object. Only one gem can be applied to a single conversation.
|
|
933
933
|
|
|
934
|
+
> [!TIP]
|
|
935
|
+
>
|
|
936
|
+
> There are some system predefined gems that by default are not shown to users (and therefore may not work properly). Use `client.fetch_gems(include_hidden=True)` to include them in the fetch result.
|
|
937
|
+
|
|
934
938
|
```python
|
|
935
939
|
async def main():
|
|
936
940
|
# Fetch all gems for the current account, including both predefined and user-created ones
|
|
937
|
-
await client.fetch_gems()
|
|
941
|
+
await client.fetch_gems(include_hidden=False)
|
|
938
942
|
|
|
939
943
|
# Once fetched, gems will be cached in `GeminiClient.gems`
|
|
940
944
|
gems = client.gems
|
|
@@ -20,17 +20,21 @@ src/gemini_webapi.egg-info/SOURCES.txt
|
|
|
20
20
|
src/gemini_webapi.egg-info/dependency_links.txt
|
|
21
21
|
src/gemini_webapi.egg-info/requires.txt
|
|
22
22
|
src/gemini_webapi.egg-info/top_level.txt
|
|
23
|
+
src/gemini_webapi/components/__init__.py
|
|
24
|
+
src/gemini_webapi/components/gem_mixin.py
|
|
23
25
|
src/gemini_webapi/types/__init__.py
|
|
24
26
|
src/gemini_webapi/types/candidate.py
|
|
25
27
|
src/gemini_webapi/types/gem.py
|
|
28
|
+
src/gemini_webapi/types/grpc.py
|
|
26
29
|
src/gemini_webapi/types/image.py
|
|
27
30
|
src/gemini_webapi/types/modeloutput.py
|
|
28
31
|
src/gemini_webapi/utils/__init__.py
|
|
32
|
+
src/gemini_webapi/utils/decorators.py
|
|
29
33
|
src/gemini_webapi/utils/get_access_token.py
|
|
30
34
|
src/gemini_webapi/utils/load_browser_cookies.py
|
|
31
35
|
src/gemini_webapi/utils/logger.py
|
|
32
36
|
src/gemini_webapi/utils/rotate_1psidts.py
|
|
33
37
|
src/gemini_webapi/utils/upload_file.py
|
|
34
38
|
tests/test_client_features.py
|
|
35
|
-
tests/
|
|
39
|
+
tests/test_gem_mixin.py
|
|
36
40
|
tests/test_save_image.py
|
|
@@ -72,7 +72,6 @@ class TestGeminiClient(unittest.IsolatedAsyncioTestCase):
|
|
|
72
72
|
self.assertTrue(response.images)
|
|
73
73
|
logger.debug(response.text)
|
|
74
74
|
for image in response.images:
|
|
75
|
-
self.assertTrue(image.url)
|
|
76
75
|
logger.debug(image)
|
|
77
76
|
|
|
78
77
|
@logger.catch(reraise=True)
|
|
@@ -83,7 +82,17 @@ class TestGeminiClient(unittest.IsolatedAsyncioTestCase):
|
|
|
83
82
|
self.assertTrue(response.images)
|
|
84
83
|
logger.debug(response.text)
|
|
85
84
|
for image in response.images:
|
|
86
|
-
|
|
85
|
+
logger.debug(image)
|
|
86
|
+
|
|
87
|
+
@logger.catch(reraise=True)
|
|
88
|
+
async def test_image_to_image(self):
|
|
89
|
+
response = await self.geminiclient.generate_content(
|
|
90
|
+
"Design an application icon based on the provided image. Make it simple and modern.",
|
|
91
|
+
files=["assets/banner.png"],
|
|
92
|
+
)
|
|
93
|
+
self.assertTrue(response.images)
|
|
94
|
+
logger.debug(response.text)
|
|
95
|
+
for image in response.images:
|
|
87
96
|
logger.debug(image)
|
|
88
97
|
|
|
89
98
|
@logger.catch(reraise=True)
|
|
@@ -95,14 +104,6 @@ class TestGeminiClient(unittest.IsolatedAsyncioTestCase):
|
|
|
95
104
|
)
|
|
96
105
|
logger.debug(response.text)
|
|
97
106
|
|
|
98
|
-
@logger.catch(reraise=True)
|
|
99
|
-
async def test_fetch_gems(self):
|
|
100
|
-
await self.geminiclient.fetch_gems()
|
|
101
|
-
gems = self.geminiclient.gems
|
|
102
|
-
self.assertTrue(len(gems.filter(predefined=True)) > 0)
|
|
103
|
-
for gem in gems:
|
|
104
|
-
logger.debug(gem.name)
|
|
105
|
-
|
|
106
107
|
@logger.catch(reraise=True)
|
|
107
108
|
async def test_thinking_model(self):
|
|
108
109
|
response = await self.geminiclient.generate_content(
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import unittest
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from gemini_webapi import GeminiClient, set_log_level, logger
|
|
6
|
+
from gemini_webapi.exceptions import AuthError
|
|
7
|
+
|
|
8
|
+
logging.getLogger("asyncio").setLevel(logging.ERROR)
|
|
9
|
+
set_log_level("DEBUG")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestGemMixin(unittest.IsolatedAsyncioTestCase):
|
|
13
|
+
async def asyncSetUp(self):
|
|
14
|
+
self.geminiclient = GeminiClient(
|
|
15
|
+
os.getenv("SECURE_1PSID"), os.getenv("SECURE_1PSIDTS"), verify=False
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
await self.geminiclient.init(timeout=60, auto_refresh=False)
|
|
20
|
+
except AuthError as e:
|
|
21
|
+
self.skipTest(e)
|
|
22
|
+
|
|
23
|
+
@logger.catch(reraise=True)
|
|
24
|
+
async def test_fetch_gems(self):
|
|
25
|
+
await self.geminiclient.fetch_gems(include_hidden=True)
|
|
26
|
+
gems = self.geminiclient.gems
|
|
27
|
+
self.assertTrue(len(gems.filter(predefined=True)) > 0)
|
|
28
|
+
for gem in gems:
|
|
29
|
+
logger.debug(gem.name)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
unittest.main()
|
|
@@ -26,8 +26,7 @@ class TestGeminiClient(unittest.IsolatedAsyncioTestCase):
|
|
|
26
26
|
"Show me some pictures of random subjects"
|
|
27
27
|
)
|
|
28
28
|
self.assertTrue(response.images)
|
|
29
|
-
for
|
|
30
|
-
self.assertTrue(image.url)
|
|
29
|
+
for image in response.images:
|
|
31
30
|
try:
|
|
32
31
|
await image.save(verbose=True, skip_invalid_filename=True)
|
|
33
32
|
except HTTPError as e:
|
|
@@ -38,8 +37,16 @@ class TestGeminiClient(unittest.IsolatedAsyncioTestCase):
|
|
|
38
37
|
"Generate a picture of random subjects"
|
|
39
38
|
)
|
|
40
39
|
self.assertTrue(response.images)
|
|
41
|
-
for
|
|
42
|
-
|
|
40
|
+
for image in response.images:
|
|
41
|
+
await image.save(verbose=True, full_size=True)
|
|
42
|
+
|
|
43
|
+
async def test_save_image_to_image(self):
|
|
44
|
+
response = await self.geminiclient.generate_content(
|
|
45
|
+
"Design an application icon based on the provided image. Make it simple and modern.",
|
|
46
|
+
files=["assets/banner.png"],
|
|
47
|
+
)
|
|
48
|
+
self.assertTrue(response.images)
|
|
49
|
+
for image in response.images:
|
|
43
50
|
await image.save(verbose=True, full_size=True)
|
|
44
51
|
|
|
45
52
|
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import asyncio
|
|
3
|
-
|
|
4
|
-
from gemini_webapi import GeminiClient, set_log_level, logger
|
|
5
|
-
|
|
6
|
-
set_log_level("DEBUG")
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@logger.catch()
|
|
10
|
-
async def main():
|
|
11
|
-
client = GeminiClient(os.getenv("SECURE_1PSID"), os.getenv("SECURE_1PSIDTS"))
|
|
12
|
-
await client.init(close_delay=30, refresh_interval=60)
|
|
13
|
-
|
|
14
|
-
while True:
|
|
15
|
-
try:
|
|
16
|
-
response = await client.generate_content("Hello world")
|
|
17
|
-
logger.info(response)
|
|
18
|
-
except Exception as e:
|
|
19
|
-
logger.error(e)
|
|
20
|
-
await asyncio.sleep(60)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if __name__ == "__main__":
|
|
24
|
-
asyncio.run(main())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi/utils/load_browser_cookies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gemini_webapi-1.14.2 → gemini_webapi-1.14.4}/src/gemini_webapi.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|