Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- name: Checkout repository
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v6

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: CPython",
"Natural Language :: English",
"Topic :: Utilities",
Expand Down
12 changes: 12 additions & 0 deletions tests/example_search_google_light_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Example: google_light search engine
import pytest
import os
import serpapi

def test_search_google_light(client):
data = client.search({
'engine': 'google_light',
'q': 'coffee',
})
assert data.get('error') is None
assert data['organic_results']
91 changes: 80 additions & 11 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
from urllib.parse import parse_qs, urlparse

import pytest

import serpapi


def _query_param(url, param):
values = parse_qs(urlparse(url).query).get(param)
if values:
return values[0]


def test_basic_import():
"""Test that basic import works as intended."""
import serpapi
Expand All @@ -18,6 +26,43 @@ def test_entrypoints(client):
assert api.locations


def test_locations_query_limit_and_shape(client):
locations = client.locations(q="Austin", limit=5)

assert isinstance(locations, list)
assert 0 < len(locations) <= 5

expected_fields = {
"id",
"name",
"canonical_name",
"country_code",
"target_type",
"reach",
}

for location in locations:
assert expected_fields.issubset(location.keys())
assert isinstance(location["canonical_name"], str)

assert any(
"Austin" in location["name"] or "Austin" in location["canonical_name"]
for location in locations
)


def test_search_accepts_location_from_locations_api(client):
locations = client.locations(q="Austin", limit=1)
location_name = locations[0]["canonical_name"]

search = client.search(q="coffee", location=location_name)

assert search.get("error") is None
assert search["organic_results"]
assert "Austin" in search["search_parameters"]["location_requested"]
assert "United States" in search["search_parameters"]["location_used"]


def test_account_without_credentials():
"""Ensure that an HTTPError is raised when account is accessed without API Credentials."""
with pytest.raises(serpapi.HTTPError):
Expand Down Expand Up @@ -58,29 +103,53 @@ def test_coffee_search_as_dict(coffee_search):
assert isinstance(d, dict)


def test_coffee_search_html(coffee_search_html):
def test_search_output_html_contains_raw_html_document(coffee_search_html):
assert isinstance(coffee_search_html, str)
assert "<html" in coffee_search_html.lower()
assert "</html>" in coffee_search_html.lower()
assert "coffee" in coffee_search_html.lower()
assert not hasattr(coffee_search_html, "next_page_url")


def test_coffee_search_n_pages(coffee_search):
page_count = 0
max_pages = 3
def test_next_page_url_uses_serpapi_pagination_next(coffee_search):
next_page_url = coffee_search.next_page_url

for page in coffee_search.yield_pages(max_pages=max_pages):
if page_count == 0:
assert 'start' not in page['search_parameters'], "The 'start' parameter should not be in the first page"

page_count += 1
assert next_page_url == coffee_search["serpapi_pagination"]["next"]
assert _query_param(next_page_url, "start") == "10"
assert _query_param(next_page_url, "api_key") is None


def test_yield_pages_returns_unique_search_pages(coffee_search):
max_pages = 3
pages = list(coffee_search.yield_pages(max_pages=max_pages))

assert page_count == max_pages
assert len(pages) == max_pages
assert len({page["search_metadata"]["id"] for page in pages}) == max_pages
assert "start" not in pages[0]["search_parameters"]
assert int(pages[1]["search_parameters"]["start"]) == 10
assert int(pages[2]["search_parameters"]["start"]) == 20


def test_coffee_search_next_page(coffee_search):
def test_next_page_advances_start_and_returns_new_results(coffee_search):
next_page = coffee_search.next_page()

assert isinstance(next_page, serpapi.SerpResults)
assert coffee_search["search_metadata"]["id"] != next_page["search_metadata"]["id"]
assert int(next_page["search_parameters"]["start"]) == 10
assert next_page["organic_results"]

page_number = next_page["search_information"].get("page_number")
if page_number is not None:
assert int(page_number) == 2


def test_search_archive_round_trips_search_id(client, coffee_search):
search_id = coffee_search["search_metadata"]["id"]

archived_search = client.search_archive(search_id=search_id)

assert isinstance(archived_search, serpapi.SerpResults)
assert archived_search["search_metadata"]["id"] == search_id


def test_search_function_signature(coffee_params, client):
Expand Down