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.
- {serpapi-1.0.2/serpapi.egg-info → serpapi-1.1.0}/PKG-INFO +35 -2
- {serpapi-1.0.2 → serpapi-1.1.0}/README.md +33 -1
- {serpapi-1.0.2 → serpapi-1.1.0}/pyproject.toml +1 -0
- serpapi-1.1.0/serpapi/__version__.py +1 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/core.py +51 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/http.py +3 -1
- {serpapi-1.0.2 → serpapi-1.1.0/serpapi.egg-info}/PKG-INFO +35 -2
- {serpapi-1.0.2 → serpapi-1.1.0}/serpapi.egg-info/SOURCES.txt +2 -0
- serpapi-1.1.0/tests/example_search_google_light_test.py +12 -0
- serpapi-1.1.0/tests/test_image_upload.py +101 -0
- serpapi-1.1.0/tests/test_integration.py +163 -0
- serpapi-1.0.2/serpapi/__version__.py +0 -1
- serpapi-1.0.2/tests/test_integration.py +0 -94
- {serpapi-1.0.2 → serpapi-1.1.0}/HISTORY.md +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/LICENSE +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/MANIFEST.in +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/docs/conf.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/__init__.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/exceptions.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/models.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/textui.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/serpapi/utils.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/serpapi.egg-info/dependency_links.txt +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/serpapi.egg-info/requires.txt +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/serpapi.egg-info/top_level.txt +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/setup.cfg +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/__init__.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/conftest.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_apple_app_store_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_baidu_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_bing_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_duckduckgo_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_ebay_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_autocomplete_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_events_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_images_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_jobs_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_local_services_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_maps_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_play_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_scholar_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_google_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_home_depot_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_naver_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_walmart_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_yahoo_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/example_search_youtube_test.py +0 -0
- {serpapi-1.0.2 → serpapi-1.1.0}/tests/test_exceptions.py +0 -0
- {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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|