gemini-webapi 1.11.1__tar.gz → 1.12.1__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.
Files changed (38) hide show
  1. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/.github/workflows/pypi-publish.yml +1 -1
  2. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/PKG-INFO +1 -1
  3. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/client.py +51 -30
  4. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/exceptions.py +8 -0
  5. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/types/candidate.py +13 -1
  6. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi.egg-info/PKG-INFO +1 -1
  7. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi.egg-info/SOURCES.txt +1 -0
  8. gemini_webapi-1.12.1/tests/test_html_entity_decode.py +48 -0
  9. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/.github/dependabot.yml +0 -0
  10. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/.github/workflows/github-release.yml +0 -0
  11. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/.gitignore +0 -0
  12. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/.vscode/launch.json +0 -0
  13. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/.vscode/settings.json +0 -0
  14. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/LICENSE +0 -0
  15. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/README.md +0 -0
  16. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/assets/banner.png +0 -0
  17. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/assets/favicon.png +0 -0
  18. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/assets/logo.svg +0 -0
  19. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/assets/sample.pdf +0 -0
  20. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/pyproject.toml +0 -0
  21. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/setup.cfg +0 -0
  22. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/__init__.py +0 -0
  23. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/constants.py +0 -0
  24. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/types/__init__.py +0 -0
  25. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/types/image.py +0 -0
  26. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/types/modeloutput.py +0 -0
  27. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/utils/__init__.py +0 -0
  28. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/utils/get_access_token.py +0 -0
  29. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/utils/load_browser_cookies.py +0 -0
  30. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/utils/logger.py +0 -0
  31. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/utils/rotate_1psidts.py +0 -0
  32. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi/utils/upload_file.py +0 -0
  33. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi.egg-info/dependency_links.txt +0 -0
  34. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi.egg-info/requires.txt +0 -0
  35. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/src/gemini_webapi.egg-info/top_level.txt +0 -0
  36. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/tests/test_client_features.py +0 -0
  37. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/tests/test_rotate_cookies.py +0 -0
  38. {gemini_webapi-1.11.1 → gemini_webapi-1.12.1}/tests/test_save_image.py +0 -0
@@ -52,7 +52,7 @@ jobs:
52
52
  id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
53
53
  steps:
54
54
  - name: Retrieve built artifacts
55
- uses: actions/download-artifact@v4.2.1
55
+ uses: actions/download-artifact@v4.3.0
56
56
  with:
57
57
  name: dist
58
58
  path: dist
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemini-webapi
3
- Version: 1.11.1
3
+ Version: 1.12.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
@@ -12,6 +12,7 @@ from .constants import Endpoint, ErrorCode, Headers, Model
12
12
  from .exceptions import (
13
13
  AuthError,
14
14
  APIError,
15
+ ImageGenerationError,
15
16
  TimeoutError,
16
17
  GeminiError,
17
18
  UsageLimitExceeded,
@@ -62,10 +63,15 @@ def running(retry: int = 0) -> callable:
62
63
  )
63
64
  else:
64
65
  return await func(client, *args, **kwargs)
65
- except APIError:
66
+ except APIError as e:
67
+ # Image generation takes too long, only retry once
68
+ if isinstance(e, ImageGenerationError):
69
+ retry = min(1, retry)
70
+
66
71
  if retry > 0:
67
72
  await asyncio.sleep(1)
68
73
  return await wrapper(client, *args, retry=retry - 1, **kwargs)
74
+
69
75
  raise
70
76
 
71
77
  return wrapper
@@ -371,11 +377,12 @@ class GeminiClient:
371
377
  response_json = json.loads(response.text.split("\n")[2])
372
378
 
373
379
  body = None
374
- for part in response_json:
380
+ body_index = 0
381
+ for part_index, part in enumerate(response_json):
375
382
  try:
376
383
  main_part = json.loads(part[2])
377
384
  if main_part[4]:
378
- body = main_part
385
+ body_index, body = part_index, main_part
379
386
  break
380
387
  except (IndexError, TypeError, ValueError):
381
388
  continue
@@ -412,7 +419,7 @@ class GeminiClient:
412
419
 
413
420
  try:
414
421
  candidates = []
415
- for i, candidate in enumerate(body[4]):
422
+ for candidate_index, candidate in enumerate(body[4]):
416
423
  text = candidate[1][0]
417
424
  if re.match(
418
425
  r"^http://googleusercontent\.com/card_content/\d+$", text
@@ -429,45 +436,59 @@ class GeminiClient:
429
436
  and candidate[12][1]
430
437
  and [
431
438
  WebImage(
432
- url=image[0][0][0],
433
- title=image[7][0],
434
- alt=image[0][4],
439
+ url=web_image[0][0][0],
440
+ title=web_image[7][0],
441
+ alt=web_image[0][4],
435
442
  proxy=self.proxy,
436
443
  )
437
- for image in candidate[12][1]
444
+ for web_image in candidate[12][1]
438
445
  ]
439
446
  or []
440
447
  )
441
448
 
442
449
  generated_images = []
443
450
  if candidate[12] and candidate[12][7] and candidate[12][7][0]:
444
- image_generation_body = json.loads(response_json[1][2])
445
- image_generation_candidate = image_generation_body[4][i]
451
+ img_body = None
452
+ for img_part_index, part in enumerate(response_json):
453
+ if img_part_index < body_index:
454
+ continue
455
+
456
+ try:
457
+ img_part = json.loads(part[2])
458
+ if img_part[4][candidate_index][12][7][0]:
459
+ img_body = img_part
460
+ break
461
+ except (IndexError, TypeError, ValueError):
462
+ continue
463
+
464
+ if not img_body:
465
+ raise ImageGenerationError(
466
+ "Failed to parse generated images. Please update gemini_webapi to the latest version. "
467
+ "If the error persists and is caused by the package, please report it on GitHub."
468
+ )
469
+
470
+ img_candidate = img_body[4][candidate_index]
471
+
446
472
  text = re.sub(
447
473
  r"http://googleusercontent\.com/image_generation_content/\d+$",
448
474
  "",
449
- image_generation_candidate[1][0],
475
+ img_candidate[1][0],
450
476
  ).rstrip()
451
477
 
452
- if (
453
- image_generation_candidate[12]
454
- and image_generation_candidate[12][7]
455
- and image_generation_candidate[12][7][0]
456
- ):
457
- generated_images = [
458
- GeneratedImage(
459
- url=image[0][3][3],
460
- title=f"[Generated Image {image[3][6]}]",
461
- alt=len(image[3][5]) > i
462
- and image[3][5][i]
463
- or image[3][5][0],
464
- proxy=self.proxy,
465
- cookies=self.cookies,
466
- )
467
- for i, image in enumerate(
468
- image_generation_candidate[12][7][0]
469
- )
470
- ]
478
+ generated_images = [
479
+ GeneratedImage(
480
+ url=generated_image[0][3][3],
481
+ title=f"[Generated Image {generated_image[3][6]}]",
482
+ alt=len(generated_image[3][5]) > image_index
483
+ and generated_image[3][5][image_index]
484
+ or generated_image[3][5][0],
485
+ proxy=self.proxy,
486
+ cookies=self.cookies,
487
+ )
488
+ for image_index, generated_image in enumerate(
489
+ img_candidate[12][7][0]
490
+ )
491
+ ]
471
492
 
472
493
  candidates.append(
473
494
  Candidate(
@@ -14,6 +14,14 @@ class APIError(Exception):
14
14
  pass
15
15
 
16
16
 
17
+ class ImageGenerationError(APIError):
18
+ """
19
+ Exception for generated image parsing errors.
20
+ """
21
+
22
+ pass
23
+
24
+
17
25
  class GeminiError(Exception):
18
26
  """
19
27
  Exception for errors returned from Gemini server which are not handled by the package.
@@ -1,4 +1,5 @@
1
- from pydantic import BaseModel
1
+ import html
2
+ from pydantic import BaseModel, field_validator
2
3
 
3
4
  from .image import Image, WebImage, GeneratedImage
4
5
 
@@ -33,6 +34,17 @@ class Candidate(BaseModel):
33
34
  def __repr__(self):
34
35
  return f"Candidate(rcid='{self.rcid}', text='{len(self.text) <= 20 and self.text or self.text[:20] + '...'}', images={self.images})"
35
36
 
37
+ @field_validator("text", "thoughts")
38
+ @classmethod
39
+ def decode_html(cls, value: str) -> str:
40
+ """
41
+ Auto unescape HTML entities in text/thoughts if any.
42
+ """
43
+
44
+ if value:
45
+ value = html.unescape(value)
46
+ return value
47
+
36
48
  @property
37
49
  def images(self) -> list[Image]:
38
50
  return self.web_images + self.generated_images
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemini-webapi
3
- Version: 1.11.1
3
+ Version: 1.12.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
@@ -31,5 +31,6 @@ src/gemini_webapi/utils/logger.py
31
31
  src/gemini_webapi/utils/rotate_1psidts.py
32
32
  src/gemini_webapi/utils/upload_file.py
33
33
  tests/test_client_features.py
34
+ tests/test_html_entity_decode.py
34
35
  tests/test_rotate_cookies.py
35
36
  tests/test_save_image.py
@@ -0,0 +1,48 @@
1
+ import unittest
2
+ from gemini_webapi.types.candidate import Candidate
3
+
4
+
5
+ class TestHtmlEntityDecode(unittest.TestCase):
6
+ def test_html_entity_decoding(self):
7
+ # Test HTML entity decoding functionality
8
+ html_encoded_text = (
9
+ "This is a code snippet: &lt;code&gt;print('Hello, World!')&lt;/code&gt;"
10
+ )
11
+ expected_decoded_text = (
12
+ "This is a code snippet: <code>print('Hello, World!')</code>"
13
+ )
14
+
15
+ # Create Candidate instance which should automatically decode HTML entities
16
+ candidate = Candidate(
17
+ rcid="test_rcid",
18
+ text=html_encoded_text,
19
+ thoughts="Testing &lt;b&gt;HTML&lt;/b&gt; entity decoding",
20
+ )
21
+
22
+ # Verify that text property is correctly decoded
23
+ self.assertEqual(candidate.text, expected_decoded_text)
24
+
25
+ # Verify that thoughts property is correctly decoded
26
+ self.assertEqual(candidate.thoughts, "Testing <b>HTML</b> entity decoding")
27
+
28
+ def test_non_html_text(self):
29
+ # Test plain text without any HTML entities
30
+ plain_text = "This is regular text with no HTML entities"
31
+
32
+ candidate = Candidate(rcid="test_rcid", text=plain_text)
33
+
34
+ # Verify the text remains unchanged
35
+ self.assertEqual(candidate.text, plain_text)
36
+
37
+ def test_complex_html_entities(self):
38
+ # Test more complex combinations of HTML entities
39
+ complex_html = "&lt;div&gt;This has &amp;amp; character\n and &quot;quotes&quot;&lt;/div&gt;"
40
+ expected_decoded = '<div>This has &amp; character\n and "quotes"</div>'
41
+
42
+ candidate = Candidate(rcid="test_rcid", text=complex_html)
43
+
44
+ self.assertEqual(candidate.text, expected_decoded)
45
+
46
+
47
+ if __name__ == "__main__":
48
+ unittest.main()
File without changes
File without changes
File without changes