serpapi 1.0.2__tar.gz → 1.1.0__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 (49) hide show
  1. {serpapi-1.0.2/serpapi.egg-info → serpapi-1.1.0}/PKG-INFO +35 -2
  2. {serpapi-1.0.2 → serpapi-1.1.0}/README.md +33 -1
  3. {serpapi-1.0.2 → serpapi-1.1.0}/pyproject.toml +1 -0
  4. serpapi-1.1.0/serpapi/__version__.py +1 -0
  5. {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/core.py +51 -0
  6. {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/http.py +3 -1
  7. {serpapi-1.0.2 → serpapi-1.1.0/serpapi.egg-info}/PKG-INFO +35 -2
  8. {serpapi-1.0.2 → serpapi-1.1.0}/serpapi.egg-info/SOURCES.txt +2 -0
  9. serpapi-1.1.0/tests/example_search_google_light_test.py +12 -0
  10. serpapi-1.1.0/tests/test_image_upload.py +101 -0
  11. serpapi-1.1.0/tests/test_integration.py +163 -0
  12. serpapi-1.0.2/serpapi/__version__.py +0 -1
  13. serpapi-1.0.2/tests/test_integration.py +0 -94
  14. {serpapi-1.0.2 → serpapi-1.1.0}/HISTORY.md +0 -0
  15. {serpapi-1.0.2 → serpapi-1.1.0}/LICENSE +0 -0
  16. {serpapi-1.0.2 → serpapi-1.1.0}/MANIFEST.in +0 -0
  17. {serpapi-1.0.2 → serpapi-1.1.0}/docs/conf.py +0 -0
  18. {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/__init__.py +0 -0
  19. {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/exceptions.py +0 -0
  20. {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/models.py +0 -0
  21. {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/textui.py +0 -0
  22. {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/utils.py +0 -0
  23. {serpapi-1.0.2 → serpapi-1.1.0}/serpapi.egg-info/dependency_links.txt +0 -0
  24. {serpapi-1.0.2 → serpapi-1.1.0}/serpapi.egg-info/requires.txt +0 -0
  25. {serpapi-1.0.2 → serpapi-1.1.0}/serpapi.egg-info/top_level.txt +0 -0
  26. {serpapi-1.0.2 → serpapi-1.1.0}/setup.cfg +0 -0
  27. {serpapi-1.0.2 → serpapi-1.1.0}/tests/__init__.py +0 -0
  28. {serpapi-1.0.2 → serpapi-1.1.0}/tests/conftest.py +0 -0
  29. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_apple_app_store_test.py +0 -0
  30. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_baidu_test.py +0 -0
  31. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_bing_test.py +0 -0
  32. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_duckduckgo_test.py +0 -0
  33. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_ebay_test.py +0 -0
  34. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_autocomplete_test.py +0 -0
  35. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_events_test.py +0 -0
  36. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_images_test.py +0 -0
  37. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_jobs_test.py +0 -0
  38. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_local_services_test.py +0 -0
  39. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_maps_test.py +0 -0
  40. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_play_test.py +0 -0
  41. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_scholar_test.py +0 -0
  42. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_test.py +0 -0
  43. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_home_depot_test.py +0 -0
  44. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_naver_test.py +0 -0
  45. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_walmart_test.py +0 -0
  46. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_yahoo_test.py +0 -0
  47. {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_youtube_test.py +0 -0
  48. {serpapi-1.0.2 → serpapi-1.1.0}/tests/test_exceptions.py +0 -0
  49. {serpapi-1.0.2 → serpapi-1.1.0}/tests/test_timeout.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: serpapi
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: The official Python client for SerpApi.com.
5
5
  Author-email: SerpApi <support@serpapi.com>
6
6
  License: MIT
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
21
22
  Classifier: Programming Language :: Python :: Implementation :: CPython
22
23
  Classifier: Natural Language :: English
23
24
  Classifier: Topic :: Utilities
@@ -313,6 +314,38 @@ results = client.search({
313
314
  ```
314
315
  - API Documentation: [serpapi.com/google-reverse-image](https://serpapi.com/google-reverse-image)
315
316
 
317
+ ### Search Google Lens by image URL or upload
318
+
319
+ Google Lens accepts either a publicly accessible image URL or an uploaded
320
+ image. To search by URL, pass the URL directly:
321
+
322
+ ```python
323
+ import os
324
+ import serpapi
325
+
326
+ client = serpapi.Client(api_key=os.getenv("SERPAPI_KEY"))
327
+ results = client.search({
328
+ "engine": "google_lens",
329
+ "url": "https://i.imgur.com/HBrB8p0.png",
330
+ })
331
+ ```
332
+
333
+ To search a local image, upload it first and pass its temporary `image_id` to
334
+ Google Lens:
335
+
336
+ ```python
337
+ upload = client.upload_image("/path/to/image.png")
338
+ results = client.search({
339
+ "engine": "google_lens",
340
+ "image_id": upload["image_id"],
341
+ })
342
+ ```
343
+
344
+ Uploaded images can be JPG/JPEG, PNG, or WebP files up to 500 KB. The returned
345
+ `image_id` expires after 10 minutes.
346
+
347
+ - API Documentation: [Google Lens image uploads](https://serpapi.com/google-lens-upload-an-image), [Image API](https://serpapi.com/image-api)
348
+
316
349
  ### Search Google Events
317
350
  ```python
318
351
  import os
@@ -416,4 +449,4 @@ Bug reports and pull requests are welcome on GitHub. Once dependencies are insta
416
449
  ```
417
450
  This triggers the [release workflow](.github/workflows/release.yml), which tests, builds, and publishes to PyPI, then smoke-tests the published package.
418
451
 
419
- > **Required secrets:** `PYPI_API_TOKEN` (PyPI upload token) and `API_KEY` (used in smoke-test live search).
452
+ > **Required secret:** `API_KEY` (used in smoke-test live search).
@@ -277,6 +277,38 @@ results = client.search({
277
277
  ```
278
278
  - API Documentation: [serpapi.com/google-reverse-image](https://serpapi.com/google-reverse-image)
279
279
 
280
+ ### Search Google Lens by image URL or upload
281
+
282
+ Google Lens accepts either a publicly accessible image URL or an uploaded
283
+ image. To search by URL, pass the URL directly:
284
+
285
+ ```python
286
+ import os
287
+ import serpapi
288
+
289
+ client = serpapi.Client(api_key=os.getenv("SERPAPI_KEY"))
290
+ results = client.search({
291
+ "engine": "google_lens",
292
+ "url": "https://i.imgur.com/HBrB8p0.png",
293
+ })
294
+ ```
295
+
296
+ To search a local image, upload it first and pass its temporary `image_id` to
297
+ Google Lens:
298
+
299
+ ```python
300
+ upload = client.upload_image("/path/to/image.png")
301
+ results = client.search({
302
+ "engine": "google_lens",
303
+ "image_id": upload["image_id"],
304
+ })
305
+ ```
306
+
307
+ Uploaded images can be JPG/JPEG, PNG, or WebP files up to 500 KB. The returned
308
+ `image_id` expires after 10 minutes.
309
+
310
+ - API Documentation: [Google Lens image uploads](https://serpapi.com/google-lens-upload-an-image), [Image API](https://serpapi.com/image-api)
311
+
280
312
  ### Search Google Events
281
313
  ```python
282
314
  import os
@@ -380,4 +412,4 @@ Bug reports and pull requests are welcome on GitHub. Once dependencies are insta
380
412
  ```
381
413
  This triggers the [release workflow](.github/workflows/release.yml), which tests, builds, and publishes to PyPI, then smoke-tests the published package.
382
414
 
383
- > **Required secrets:** `PYPI_API_TOKEN` (PyPI upload token) and `API_KEY` (used in smoke-test live search).
415
+ > **Required secret:** `API_KEY` (used in smoke-test live search).
@@ -28,6 +28,7 @@ classifiers = [
28
28
  "Programming Language :: Python :: 3.11",
29
29
  "Programming Language :: Python :: 3.12",
30
30
  "Programming Language :: Python :: 3.13",
31
+ "Programming Language :: Python :: 3.14",
31
32
  "Programming Language :: Python :: Implementation :: CPython",
32
33
  "Natural Language :: English",
33
34
  "Topic :: Utilities",
@@ -0,0 +1 @@
1
+ __version__ = "1.1.0"
@@ -1,3 +1,6 @@
1
+ import io
2
+ import os
3
+
1
4
  from .http import HTTPClient
2
5
  from .exceptions import SearchIDNotProvided
3
6
  from .models import SerpResults
@@ -108,6 +111,53 @@ class Client(HTTPClient):
108
111
  r = self.request("GET", f"/searches/{ search_id }", params=params, **request_kwargs)
109
112
  return SerpResults.from_http_response(r, client=self)
110
113
 
114
+ def upload_image(self, image, **kwargs):
115
+ """Upload an image to SerpApi's Image API.
116
+
117
+ ``image`` can be a filesystem path or an open binary file object. The
118
+ returned dictionary contains an ``image_id`` that can be passed to
119
+ :meth:`search` for engines that accept uploaded images, such as Google
120
+ Lens.
121
+
122
+ :param image: a path or open binary file object containing a JPG/JPEG,
123
+ PNG, or WebP image no larger than 500 KB.
124
+ :param api_key: the API Key to use for SerpApi.com.
125
+ :param **: any additional multipart form fields to pass to the API.
126
+
127
+ **Learn more**: https://serpapi.com/image-api
128
+ """
129
+ request_kwargs = {}
130
+ for key in ["timeout", "proxies", "verify", "stream", "cert"]:
131
+ if key in kwargs:
132
+ request_kwargs[key] = kwargs.pop(key)
133
+
134
+ data = kwargs
135
+ if "api_key" not in data:
136
+ data["api_key"] = self.api_key
137
+
138
+ image_file = None
139
+ try:
140
+ if isinstance(image, (str, os.PathLike)):
141
+ image_file = open(image, "rb")
142
+ image = image_file
143
+ elif isinstance(image, io.TextIOBase):
144
+ raise TypeError(
145
+ "image file must be opened in binary mode, e.g. open(path, 'rb')"
146
+ )
147
+
148
+ r = self.request(
149
+ "POST",
150
+ "/image",
151
+ params={},
152
+ data=data,
153
+ files={"image": image},
154
+ **request_kwargs,
155
+ )
156
+ return r.json()
157
+ finally:
158
+ if image_file is not None:
159
+ image_file.close()
160
+
111
161
  def locations(self, params: dict = None, **kwargs):
112
162
  """Get a list of supported Google locations.
113
163
 
@@ -168,5 +218,6 @@ class Client(HTTPClient):
168
218
  _client = Client()
169
219
  search = _client.search
170
220
  search_archive = _client.search_archive
221
+ upload_image = _client.upload_image
171
222
  locations = _client.locations
172
223
  account = _client.account
@@ -23,7 +23,9 @@ class HTTPClient:
23
23
 
24
24
  def request(self, method, path, params, *, assert_200=True, **kwargs):
25
25
  # Inject the API Key into the params.
26
- if "api_key" not in params:
26
+ request_data = kwargs.get("data")
27
+ api_key_in_data = isinstance(request_data, dict) and "api_key" in request_data
28
+ if "api_key" not in params and not api_key_in_data:
27
29
  params["api_key"] = self.api_key
28
30
 
29
31
  # Build the URL, as needed.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: serpapi
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: The official Python client for SerpApi.com.
5
5
  Author-email: SerpApi <support@serpapi.com>
6
6
  License: MIT
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
21
22
  Classifier: Programming Language :: Python :: Implementation :: CPython
22
23
  Classifier: Natural Language :: English
23
24
  Classifier: Topic :: Utilities
@@ -313,6 +314,38 @@ results = client.search({
313
314
  ```
314
315
  - API Documentation: [serpapi.com/google-reverse-image](https://serpapi.com/google-reverse-image)
315
316
 
317
+ ### Search Google Lens by image URL or upload
318
+
319
+ Google Lens accepts either a publicly accessible image URL or an uploaded
320
+ image. To search by URL, pass the URL directly:
321
+
322
+ ```python
323
+ import os
324
+ import serpapi
325
+
326
+ client = serpapi.Client(api_key=os.getenv("SERPAPI_KEY"))
327
+ results = client.search({
328
+ "engine": "google_lens",
329
+ "url": "https://i.imgur.com/HBrB8p0.png",
330
+ })
331
+ ```
332
+
333
+ To search a local image, upload it first and pass its temporary `image_id` to
334
+ Google Lens:
335
+
336
+ ```python
337
+ upload = client.upload_image("/path/to/image.png")
338
+ results = client.search({
339
+ "engine": "google_lens",
340
+ "image_id": upload["image_id"],
341
+ })
342
+ ```
343
+
344
+ Uploaded images can be JPG/JPEG, PNG, or WebP files up to 500 KB. The returned
345
+ `image_id` expires after 10 minutes.
346
+
347
+ - API Documentation: [Google Lens image uploads](https://serpapi.com/google-lens-upload-an-image), [Image API](https://serpapi.com/image-api)
348
+
316
349
  ### Search Google Events
317
350
  ```python
318
351
  import os
@@ -416,4 +449,4 @@ Bug reports and pull requests are welcome on GitHub. Once dependencies are insta
416
449
  ```
417
450
  This triggers the [release workflow](.github/workflows/release.yml), which tests, builds, and publishes to PyPI, then smoke-tests the published package.
418
451
 
419
- > **Required secrets:** `PYPI_API_TOKEN` (PyPI upload token) and `API_KEY` (used in smoke-test live search).
452
+ > **Required secret:** `API_KEY` (used in smoke-test live search).
@@ -28,6 +28,7 @@ tests/example_search_google_autocomplete_test.py
28
28
  tests/example_search_google_events_test.py
29
29
  tests/example_search_google_images_test.py
30
30
  tests/example_search_google_jobs_test.py
31
+ tests/example_search_google_light_test.py
31
32
  tests/example_search_google_local_services_test.py
32
33
  tests/example_search_google_maps_test.py
33
34
  tests/example_search_google_play_test.py
@@ -39,5 +40,6 @@ tests/example_search_walmart_test.py
39
40
  tests/example_search_yahoo_test.py
40
41
  tests/example_search_youtube_test.py
41
42
  tests/test_exceptions.py
43
+ tests/test_image_upload.py
42
44
  tests/test_integration.py
43
45
  tests/test_timeout.py
@@ -0,0 +1,12 @@
1
+ # Example: google_light search engine
2
+ import pytest
3
+ import os
4
+ import serpapi
5
+
6
+ def test_search_google_light(client):
7
+ data = client.search({
8
+ 'engine': 'google_light',
9
+ 'q': 'coffee',
10
+ })
11
+ assert data.get('error') is None
12
+ assert data['organic_results']
@@ -0,0 +1,101 @@
1
+ from io import BytesIO, StringIO
2
+ from unittest.mock import Mock
3
+
4
+ import pytest
5
+ import requests
6
+
7
+ import serpapi
8
+
9
+
10
+ def json_response(data):
11
+ response = requests.Response()
12
+ response.status_code = 200
13
+ response._content = data
14
+ return response
15
+
16
+
17
+ def test_upload_image_path_sends_multipart_request(tmp_path):
18
+ image_path = tmp_path / "test.png"
19
+ image_path.write_bytes(b"fake-png-data")
20
+ client = serpapi.Client(api_key="test-api-key")
21
+
22
+ def request(**kwargs):
23
+ assert kwargs["method"] == "POST"
24
+ assert kwargs["url"] == "https://serpapi.com/image"
25
+ assert kwargs["params"] == {}
26
+ assert kwargs["data"] == {"api_key": "test-api-key"}
27
+ assert kwargs["files"]["image"].name == str(image_path)
28
+ assert kwargs["files"]["image"].read() == b"fake-png-data"
29
+ return json_response(
30
+ b'{"message": "Image uploaded successfully.", "image_id": "image-123"}'
31
+ )
32
+
33
+ client.session.request = Mock(side_effect=request)
34
+
35
+ result = client.upload_image(image_path)
36
+
37
+ assert result["image_id"] == "image-123"
38
+
39
+
40
+ def test_upload_image_accepts_open_binary_file_and_request_options():
41
+ image = BytesIO(b"fake-image-data")
42
+ client = serpapi.Client(api_key="client-api-key", timeout=10)
43
+ client.session.request = Mock(
44
+ return_value=json_response(b'{"image_id": "image-456"}')
45
+ )
46
+
47
+ result = client.upload_image(
48
+ image,
49
+ api_key="request-api-key",
50
+ timeout=5,
51
+ zero_trace="true",
52
+ )
53
+
54
+ assert result == {"image_id": "image-456"}
55
+ assert not image.closed
56
+ _, request_kwargs = client.session.request.call_args
57
+ assert request_kwargs["params"] == {}
58
+ assert request_kwargs["data"] == {
59
+ "api_key": "request-api-key",
60
+ "zero_trace": "true",
61
+ }
62
+ assert request_kwargs["files"] == {"image": image}
63
+ assert request_kwargs["timeout"] == 5
64
+
65
+
66
+ def test_upload_image_rejects_text_mode_file(tmp_path):
67
+ image_path = tmp_path / "test.png"
68
+ image_path.write_text("not binary image data")
69
+ client = serpapi.Client(api_key="test-api-key")
70
+ client.session.request = Mock()
71
+
72
+ with image_path.open("r") as image:
73
+ with pytest.raises(TypeError, match="opened in binary mode"):
74
+ client.upload_image(image)
75
+
76
+ client.session.request.assert_not_called()
77
+
78
+
79
+ def test_upload_image_rejects_string_io():
80
+ client = serpapi.Client(api_key="test-api-key")
81
+ client.session.request = Mock()
82
+
83
+ with pytest.raises(TypeError, match="opened in binary mode"):
84
+ client.upload_image(StringIO("not binary image data"))
85
+
86
+ client.session.request.assert_not_called()
87
+
88
+
89
+ def test_request_injects_api_key_when_form_data_does_not_include_it():
90
+ client = serpapi.Client(api_key="test-api-key")
91
+ client.session.request = Mock(return_value=json_response(b"{}"))
92
+
93
+ client.request("POST", "/example", params={}, data={"field": "value"})
94
+
95
+ _, request_kwargs = client.session.request.call_args
96
+ assert request_kwargs["params"] == {"api_key": "test-api-key"}
97
+ assert request_kwargs["data"] == {"field": "value"}
98
+
99
+
100
+ def test_module_exposes_upload_image_entrypoint():
101
+ assert callable(serpapi.upload_image)
@@ -0,0 +1,163 @@
1
+ from urllib.parse import parse_qs, urlparse
2
+
3
+ import pytest
4
+
5
+ import serpapi
6
+
7
+
8
+ def _query_param(url, param):
9
+ values = parse_qs(urlparse(url).query).get(param)
10
+ if values:
11
+ return values[0]
12
+
13
+
14
+ def test_basic_import():
15
+ """Test that basic import works as intended."""
16
+ import serpapi
17
+
18
+
19
+ def test_entrypoints(client):
20
+ """Test that pure references to the publicly accessible API surface introduces no errors."""
21
+
22
+ for api in [client, serpapi]:
23
+ assert api.account
24
+ assert api.search
25
+ assert api.search_archive
26
+ assert api.locations
27
+
28
+
29
+ def test_locations_query_limit_and_shape(client):
30
+ locations = client.locations(q="Austin", limit=5)
31
+
32
+ assert isinstance(locations, list)
33
+ assert 0 < len(locations) <= 5
34
+
35
+ expected_fields = {
36
+ "id",
37
+ "name",
38
+ "canonical_name",
39
+ "country_code",
40
+ "target_type",
41
+ "reach",
42
+ }
43
+
44
+ for location in locations:
45
+ assert expected_fields.issubset(location.keys())
46
+ assert isinstance(location["canonical_name"], str)
47
+
48
+ assert any(
49
+ "Austin" in location["name"] or "Austin" in location["canonical_name"]
50
+ for location in locations
51
+ )
52
+
53
+
54
+ def test_search_accepts_location_from_locations_api(client):
55
+ locations = client.locations(q="Austin", limit=1)
56
+ location_name = locations[0]["canonical_name"]
57
+
58
+ search = client.search(q="coffee", location=location_name)
59
+
60
+ assert search.get("error") is None
61
+ assert search["organic_results"]
62
+ assert "Austin" in search["search_parameters"]["location_requested"]
63
+ assert "United States" in search["search_parameters"]["location_used"]
64
+
65
+
66
+ def test_account_without_credentials():
67
+ """Ensure that an HTTPError is raised when account is accessed without API Credentials."""
68
+ with pytest.raises(serpapi.HTTPError):
69
+ serpapi.account()
70
+
71
+
72
+ def test_account_with_bad_credentials(invalid_key_client):
73
+ """Ensure that an HTTPError is raised when account is accessed with invalid API Credentials."""
74
+ with pytest.raises(serpapi.HTTPError) as exc_info:
75
+ invalid_key_client.account()
76
+
77
+ assert exc_info.value.response.status_code == 401
78
+
79
+
80
+ def test_account_with_credentials(client):
81
+ """Ensure that account appears to be returning valid data if the API Key is correct."""
82
+ account = client.account()
83
+ assert account
84
+ assert account.keys()
85
+ assert isinstance(account, dict)
86
+
87
+
88
+ def test_search_with_missing_params(client):
89
+ with pytest.raises(serpapi.HTTPError) as exc_info:
90
+ client.search({ "q": "" })
91
+
92
+ assert exc_info.value.status_code == 400
93
+ assert "Missing query `q` parameter" in exc_info.value.error
94
+
95
+
96
+ def test_coffee_search(coffee_search):
97
+ assert isinstance(coffee_search, serpapi.SerpResults)
98
+ assert hasattr(coffee_search, "__getitem__")
99
+
100
+
101
+ def test_coffee_search_as_dict(coffee_search):
102
+ d = coffee_search.as_dict()
103
+ assert isinstance(d, dict)
104
+
105
+
106
+ def test_search_output_html_contains_raw_html_document(coffee_search_html):
107
+ assert isinstance(coffee_search_html, str)
108
+ assert "<html" in coffee_search_html.lower()
109
+ assert "</html>" in coffee_search_html.lower()
110
+ assert "coffee" in coffee_search_html.lower()
111
+ assert not hasattr(coffee_search_html, "next_page_url")
112
+
113
+
114
+ def test_next_page_url_uses_serpapi_pagination_next(coffee_search):
115
+ next_page_url = coffee_search.next_page_url
116
+
117
+ assert next_page_url == coffee_search["serpapi_pagination"]["next"]
118
+ assert _query_param(next_page_url, "start") == "10"
119
+ assert _query_param(next_page_url, "api_key") is None
120
+
121
+
122
+ def test_yield_pages_returns_unique_search_pages(coffee_search):
123
+ max_pages = 3
124
+ pages = list(coffee_search.yield_pages(max_pages=max_pages))
125
+
126
+ assert len(pages) == max_pages
127
+ assert len({page["search_metadata"]["id"] for page in pages}) == max_pages
128
+ assert "start" not in pages[0]["search_parameters"]
129
+ assert int(pages[1]["search_parameters"]["start"]) == 10
130
+ assert int(pages[2]["search_parameters"]["start"]) == 20
131
+
132
+
133
+ def test_next_page_advances_start_and_returns_new_results(coffee_search):
134
+ next_page = coffee_search.next_page()
135
+
136
+ assert isinstance(next_page, serpapi.SerpResults)
137
+ assert coffee_search["search_metadata"]["id"] != next_page["search_metadata"]["id"]
138
+ assert int(next_page["search_parameters"]["start"]) == 10
139
+ assert next_page["organic_results"]
140
+
141
+ page_number = next_page["search_information"].get("page_number")
142
+ if page_number is not None:
143
+ assert int(page_number) == 2
144
+
145
+
146
+ def test_search_archive_round_trips_search_id(client, coffee_search):
147
+ search_id = coffee_search["search_metadata"]["id"]
148
+
149
+ archived_search = client.search_archive(search_id=search_id)
150
+
151
+ assert isinstance(archived_search, serpapi.SerpResults)
152
+ assert archived_search["search_metadata"]["id"] == search_id
153
+
154
+
155
+ def test_search_function_signature(coffee_params, client):
156
+ s = client.search(coffee_params)
157
+ assert s["search_metadata"]["id"]
158
+
159
+ s = client.search(**coffee_params)
160
+ assert s["search_metadata"]["id"]
161
+
162
+ s = client.search(q='coffee')
163
+ assert s["search_metadata"]["id"]
@@ -1 +0,0 @@
1
- __version__ = "1.0.2"
@@ -1,94 +0,0 @@
1
- import pytest
2
-
3
- import serpapi
4
-
5
-
6
- def test_basic_import():
7
- """Test that basic import works as intended."""
8
- import serpapi
9
-
10
-
11
- def test_entrypoints(client):
12
- """Test that pure references to the publicly accessible API surface introduces no errors."""
13
-
14
- for api in [client, serpapi]:
15
- assert api.account
16
- assert api.search
17
- assert api.search_archive
18
- assert api.locations
19
-
20
-
21
- def test_account_without_credentials():
22
- """Ensure that an HTTPError is raised when account is accessed without API Credentials."""
23
- with pytest.raises(serpapi.HTTPError):
24
- serpapi.account()
25
-
26
-
27
- def test_account_with_bad_credentials(invalid_key_client):
28
- """Ensure that an HTTPError is raised when account is accessed with invalid API Credentials."""
29
- with pytest.raises(serpapi.HTTPError) as exc_info:
30
- invalid_key_client.account()
31
-
32
- assert exc_info.value.response.status_code == 401
33
-
34
-
35
- def test_account_with_credentials(client):
36
- """Ensure that account appears to be returning valid data if the API Key is correct."""
37
- account = client.account()
38
- assert account
39
- assert account.keys()
40
- assert isinstance(account, dict)
41
-
42
-
43
- def test_search_with_missing_params(client):
44
- with pytest.raises(serpapi.HTTPError) as exc_info:
45
- client.search({ "q": "" })
46
-
47
- assert exc_info.value.status_code == 400
48
- assert "Missing query `q` parameter" in exc_info.value.error
49
-
50
-
51
- def test_coffee_search(coffee_search):
52
- assert isinstance(coffee_search, serpapi.SerpResults)
53
- assert hasattr(coffee_search, "__getitem__")
54
-
55
-
56
- def test_coffee_search_as_dict(coffee_search):
57
- d = coffee_search.as_dict()
58
- assert isinstance(d, dict)
59
-
60
-
61
- def test_coffee_search_html(coffee_search_html):
62
- assert isinstance(coffee_search_html, str)
63
- assert not hasattr(coffee_search_html, "next_page_url")
64
-
65
-
66
- def test_coffee_search_n_pages(coffee_search):
67
- page_count = 0
68
- max_pages = 3
69
-
70
- for page in coffee_search.yield_pages(max_pages=max_pages):
71
- if page_count == 0:
72
- assert 'start' not in page['search_parameters'], "The 'start' parameter should not be in the first page"
73
-
74
- page_count += 1
75
-
76
- assert page_count == max_pages
77
-
78
-
79
- def test_coffee_search_next_page(coffee_search):
80
- next_page = coffee_search.next_page()
81
-
82
- assert isinstance(next_page, serpapi.SerpResults)
83
- assert coffee_search["search_metadata"]["id"] != next_page["search_metadata"]["id"]
84
-
85
-
86
- def test_search_function_signature(coffee_params, client):
87
- s = client.search(coffee_params)
88
- assert s["search_metadata"]["id"]
89
-
90
- s = client.search(**coffee_params)
91
- assert s["search_metadata"]["id"]
92
-
93
- s = client.search(q='coffee')
94
- assert s["search_metadata"]["id"]
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