Skip to content

Commit 810302a

Browse files
committed
Enhance type hinting and add py.typed for better type support; refactor prettify_json function
1 parent 561d334 commit 810302a

6 files changed

Lines changed: 51 additions & 45 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,8 @@ version = { attr = "serpapi.__version__.__version__" }
5151
[tool.setuptools.packages.find]
5252
exclude = ["tests", "tests.*"]
5353

54+
[tool.setuptools.package-data]
55+
"serpapi" = ["py.typed"]
56+
5457
[tool.pytest.ini_options]
5558
testpaths = ["tests"]

serpapi/core.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Any, Dict, Optional, Union
2+
13
from .http import HTTPClient
24
from .exceptions import SearchIDNotProvided
35
from .models import SerpResults
@@ -25,13 +27,13 @@ class Client(HTTPClient):
2527

2628
DASHBOARD_URL = "https://serpapi.com/dashboard"
2729

28-
def __init__(self, *, api_key=None, timeout=None):
30+
def __init__(self, *, api_key: Optional[str] = None, timeout: Optional[float] = None) -> None:
2931
super().__init__(api_key=api_key, timeout=timeout)
3032

3133
def __repr__(self):
3234
return "<SerpApi Client>"
3335

34-
def search(self, params: dict = None, **kwargs):
36+
def search(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Union[SerpResults, str]:
3537
"""Fetch a page of results from SerpApi. Returns a :class:`SerpResults <serpapi.client.SerpResults>` object, or unicode text (*e.g.* if ``'output': 'html'`` was passed).
3638
3739
The following three calls are equivalent:
@@ -72,11 +74,11 @@ def search(self, params: dict = None, **kwargs):
7274
if kwargs:
7375
params.update(kwargs)
7476

75-
r = self.request("GET", "/search", params=params, **request_kwargs)
77+
r = self.request("GET", "/search", params=params, assert_200=True, **request_kwargs)
7678

7779
return SerpResults.from_http_response(r, client=self)
7880

79-
def search_archive(self, params: dict = None, **kwargs):
81+
def search_archive(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Union[SerpResults, str]:
8082
"""Get a result from the SerpApi Search Archive API.
8183
8284
:param search_id: the Search ID of the search to retrieve from the archive.
@@ -105,10 +107,10 @@ def search_archive(self, params: dict = None, **kwargs):
105107
f"Please provide 'search_id', found here: { self.DASHBOARD_URL }"
106108
)
107109

108-
r = self.request("GET", f"/searches/{ search_id }", params=params, **request_kwargs)
110+
r = self.request("GET", f"/searches/{ search_id }", params=params, assert_200=True, **request_kwargs)
109111
return SerpResults.from_http_response(r, client=self)
110112

111-
def locations(self, params: dict = None, **kwargs):
113+
def locations(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Any:
112114
"""Get a list of supported Google locations.
113115
114116
@@ -139,7 +141,7 @@ def locations(self, params: dict = None, **kwargs):
139141
)
140142
return r.json()
141143

142-
def account(self, params: dict = None, **kwargs):
144+
def account(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Any:
143145
"""Get SerpApi account information.
144146
145147
:param api_key: the API Key to use for SerpApi.com.

serpapi/http.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import requests
2+
from typing import Any, Dict, Optional
23

34
from .exceptions import (
45
HTTPError,
@@ -14,14 +15,14 @@ class HTTPClient:
1415
BASE_DOMAIN = "https://serpapi.com"
1516
USER_AGENT = f"serpapi-python, v{__version__}"
1617

17-
def __init__(self, *, api_key=None, timeout=None):
18+
def __init__(self, *, api_key: Optional[str] = None, timeout: Optional[float] = None) -> None:
1819
# Used to authenticate requests.
1920
# TODO: do we want to support the environment variable? Seems like a security risk.
2021
self.api_key = api_key
2122
self.timeout = timeout
2223
self.session = requests.Session()
2324

24-
def request(self, method, path, params, *, assert_200=True, **kwargs):
25+
def request(self, method: str, path: str, params: Dict[str, Any], *, assert_200: bool = True, **kwargs: Any) -> requests.Response:
2526
# Inject the API Key into the params.
2627
if "api_key" not in params:
2728
params["api_key"] = self.api_key
@@ -59,7 +60,7 @@ def request(self, method, path, params, *, assert_200=True, **kwargs):
5960
return r
6061

6162

62-
def raise_for_status(r):
63+
def raise_for_status(r: requests.Response) -> None:
6364
"""Raise an exception if the status code is not 200."""
6465
# TODO: put custom behavior in here for various status codes.
6566

serpapi/models.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import json
2+
from typing import Any, Dict, Iterator, Optional, Union, TYPE_CHECKING
23

3-
from pprint import pformat
44
from collections import UserDict
55

6+
import requests
7+
68
from .textui import prettify_json
7-
from .exceptions import HTTPError
9+
10+
if TYPE_CHECKING:
11+
from .core import Client
812

913

10-
class SerpResults(UserDict):
14+
class SerpResults(UserDict[str, Any]):
1115
"""A dictionary-like object that represents the results of a SerpApi request.
1216
1317
.. code-block:: python
@@ -21,68 +25,71 @@ class SerpResults(UserDict):
2125
It can be used like a dictionary, but also has some additional methods.
2226
"""
2327

24-
def __init__(self, data, *, client):
28+
def __init__(self, data: Dict[str, Any], *, client: Optional["Client"]) -> None:
2529
super().__init__(data)
2630
self.client = client
2731

28-
def __getstate__(self):
32+
def __getstate__(self) -> Dict[str, Any]:
2933
return self.data
3034

31-
def __setstate__(self, state):
35+
def __setstate__(self, state: Dict[str, Any]) -> None:
3236
self.data = state
3337

34-
def __repr__(self):
38+
def __repr__(self) -> str:
3539
"""The visual representation of the data, which is pretty printed, for
3640
ease of use.
3741
"""
3842

3943
return prettify_json(json.dumps(self.data, indent=4))
4044

41-
def as_dict(self):
45+
def as_dict(self) -> Dict[str, Any]:
4246
"""Returns the data as a standard Python dictionary.
4347
This can be useful when using ``json.dumps(search), for example."""
4448

4549
return self.data.copy()
4650

4751
@property
48-
def next_page_url(self):
52+
def next_page_url(self) -> Optional[str]:
4953
"""The URL of the next page of results, if any."""
5054

51-
serpapi_pagination = self.data.get("serpapi_pagination")
55+
serpapi_pagination: Optional[dict[str, Any]] = self.data.get("serpapi_pagination")
5256

5357
if serpapi_pagination:
54-
return serpapi_pagination.get("next")
58+
next_url = serpapi_pagination.get("next")
59+
return next_url if isinstance(next_url, str) else None
60+
return None
5561

56-
def next_page(self):
62+
def next_page(self) -> Optional[Union["SerpResults", str]]:
5763
"""Return the next page of results, if any."""
5864

59-
if self.next_page_url:
65+
if self.next_page_url and self.client is not None:
6066
# Include support for the API key, as it is not included in the next page URL.
6167
params = {"api_key": self.client.api_key}
6268

6369
r = self.client.request("GET", path=self.next_page_url, params=params)
6470
return SerpResults.from_http_response(r, client=self.client)
6571

66-
def yield_pages(self, max_pages=1_000):
72+
return None
73+
74+
def yield_pages(self, max_pages: int = 1_000) -> Iterator[Union["SerpResults", str]]:
6775
"""A generator that ``yield`` s the next ``n`` pages of search results, if any.
6876
6977
:param max_pages: limit the number of pages yielded to ``n``.
7078
"""
7179

7280
current_page_count = 0
73-
74-
current_page = self
81+
current_page: Union["SerpResults", str, None] = self
7582
while current_page and current_page_count < max_pages:
7683
yield current_page
7784
current_page_count += 1
78-
if current_page.next_page_url:
85+
if isinstance(current_page, SerpResults) and current_page.next_page_url:
7986
current_page = current_page.next_page()
8087
else:
8188
break
8289

8390

8491
@classmethod
85-
def from_http_response(cls, r, *, client=None):
92+
def from_http_response(cls, r: requests.Response, *, client: Optional["Client"] = None) -> Union["SerpResults", str]:
8693
"""Construct a SerpResults object from an HTTP response.
8794
8895
:param assert_200: if ``True`` (default), raise an exception if the status code is not 200.
@@ -93,9 +100,9 @@ def from_http_response(cls, r, *, client=None):
93100
"""
94101

95102
try:
96-
cls = cls(r.json(), client=client)
103+
inst = cls(r.json(), client=client)
97104

98-
return cls
105+
return inst
99106
except ValueError:
100107
# If the response is not JSON, return the raw text.
101108
return r.text

serpapi/py.typed

Whitespace-only changes.

serpapi/textui.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
1-
try:
2-
import pygments
3-
from pygments import highlight, lexers, formatters
4-
except ImportError:
5-
pygments = None
6-
7-
8-
def prettify_json(s):
9-
if pygments:
10-
return highlight(
11-
s,
12-
lexers.JsonLexer(),
13-
formatters.TerminalFormatter(),
14-
)
15-
else:
1+
def prettify_json(s: str) -> str:
2+
try:
3+
from pygments import highlight
4+
from pygments.lexers import get_lexer_by_name #type: ignore
5+
from pygments.formatters import TerminalFormatter
6+
except ImportError:
167
return s
8+
9+
return highlight(s, get_lexer_by_name("JSON"), TerminalFormatter())

0 commit comments

Comments
 (0)