diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a79632d..2f220fd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,14 +15,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
@@ -30,7 +30,13 @@ jobs:
- name: Install dependencies
run: pip install -e .[test]
- - name: Test with pytest
- run: pytest
+ - name: Run tests
+ run: pytest -k "not example"
env:
- API_KEY: ${{secrets.API_KEY}}
+ API_KEY: ${{ secrets.API_KEY }}
+
+ - name: Run example tests
+ if: matrix.python-version == '3.13'
+ run: pytest -k "example"
+ env:
+ API_KEY: ${{ secrets.API_KEY }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..c3c5335
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,137 @@
+name: Release
+
+on:
+ push:
+ tags: ['v**']
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ test:
+ name: Test / Python ${{ matrix.python-version }}
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: actions/setup-python@v6
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ run: pip install -e .[test]
+
+ - name: Run tests
+ run: pytest -k "not example"
+ env:
+ API_KEY: ${{ secrets.API_KEY }}
+
+ - name: Run example tests
+ if: matrix.python-version == '3.13'
+ run: pytest -k "example"
+ env:
+ API_KEY: ${{ secrets.API_KEY }}
+
+ build:
+ name: Build distribution
+ needs: [test]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+
+ - name: Build sdist and wheel
+ run: |
+ pip install build
+ python -m build
+
+ - uses: actions/upload-artifact@v7
+ with:
+ name: dist
+ path: dist/
+ if-no-files-found: error
+
+ publish:
+ name: Publish release
+ needs: [build]
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ packages: write
+ steps:
+ - uses: actions/download-artifact@v8
+ with:
+ name: dist
+ path: dist/
+
+ - uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+
+ - name: Create GitHub Release
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_REPO: ${{ github.repository }}
+ run: |
+ gh release create ${{ github.ref_name }} dist/* --generate-notes \
+ || gh release upload ${{ github.ref_name }} dist/* --clobber
+
+ - name: Install twine
+ run: pip install --upgrade pip twine
+
+ - name: Publish to PyPI
+ env:
+ TWINE_USERNAME: __token__
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
+ run: twine upload dist/*
+
+ smoke-test:
+ name: Smoke test published package
+ needs: [publish]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+
+ - name: Wait for PyPI availability
+ run: |
+ VERSION=${GITHUB_REF_NAME#v}
+ echo "Waiting for serpapi==$VERSION on PyPI..."
+ for i in $(seq 1 12); do
+ if pip index versions serpapi 2>/dev/null | grep -q "$VERSION"; then
+ echo "Available!"
+ exit 0
+ fi
+ echo "Attempt $i/12 — sleeping 10s..."
+ sleep 10
+ done
+ echo "Timed out waiting for PyPI propagation" && exit 1
+
+ - name: Install from PyPI
+ run: |
+ VERSION=${GITHUB_REF_NAME#v}
+ pip install "serpapi==$VERSION"
+
+ - name: Verify import and version
+ env:
+ API_KEY: ${{ secrets.API_KEY }}
+ EXPECTED_VERSION: ${{ github.ref_name }}
+ shell: python3 {0}
+ run: |
+ import os
+ import serpapi
+ expected = os.environ["EXPECTED_VERSION"].lstrip("v")
+ assert serpapi.__version__ == expected, f"Version mismatch: {serpapi.__version__} != {expected}"
+ print(f"OK: serpapi=={serpapi.__version__} installed successfully")
+ client = serpapi.Client(api_key=os.environ["API_KEY"])
+ results = client.search({"engine": "google", "q": "coffee"})
+ assert results.get("organic_results"), "No organic results returned"
+ print(f"OK: live search returned {len(results['organic_results'])} organic results")
diff --git a/.gitignore b/.gitignore
index 1d0adf0..143977d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ __pycache__
.vscode
t.py
+.venv/
diff --git a/HISTORY.md b/HISTORY.md
index a34d285..fc96f64 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,12 +1,51 @@
Release History
===============
-dev
----
+1.0.1 (2026-03-18)
+------------------
-* Initial release
+- Fix release workflow: YAML syntax error in smoke test step and remove broken GitHub Packages publish.
-1.0.0 (planned)
----------------
+1.0.0 (2026-03-18)
+------------------
-- First planned release
\ No newline at end of file
+- Automated PyPI release pipeline via GitHub Actions (tag-triggered: test → build → publish → smoke test).
+- Modernized packaging to PEP 621 (pyproject.toml), removing legacy setup.py and Pipfile.
+- Added Python 3.13 support.
+
+0.1.6 (2026-02-16)
+------------------
+
+- Add support for request timeouts.
+- Add status and error codes support - https://serpapi.com/api-status-and-error-codes
+
+0.1.5 (2023-11-01)
+------------------
+
+- Python 3.12 support.
+
+0.1.4 (2023-10-11)
+------------------
+
+- Add README documentation for various engines.
+
+0.1.3 (2023-10-06)
+------------------
+
+- Replace deprecated serpapi_pagination.next_link with 'next'.
+- Improve documentation: how to use the client directly for pagination searches.
+
+0.1.2 (2023-10-03)
+------------------
+
+- Update project status to Production/Stable.
+
+0.1.1 (2023-10-03)
+------------------
+
+- Update documentation link to point to Read the Docs.
+
+0.1.0 (2023-10-03)
+------------------
+
+- First release on PyPI.
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 2a2842a..0000000
--- a/Makefile
+++ /dev/null
@@ -1,69 +0,0 @@
-# Automate pip package development
-#
-# Usage
-# To release a package.
-# - update version in serpapi/_version.py
-# - review README version
-# - run
-# $ make release
-
-# current version
-version=$(shell grep version setup.py | cut -d"'" -f2)
-dist=dist/serpapi-$(version).tar.gz
-
-.PHONY: build
-
-all: clean install readme doc lint test build oobt check
-
-clean:
- find . -name '*.pyc' -delete
- find . -type d -name "__pycache__" -delete
- python3 -m pip uninstall serpapi
-
-# lint check
-lint:
- python3 -m pylint serpapi
-
-# test with Python 3
-test:
- python3 -mpytest --cov=serpapi --cov-report html tests/*.py
-
-# install dependencies
-#
-# pytest-cov - code coverage extension for pytest
-# sphinx - documentation
-# twine - release automation
-install:
- python3 -m pip install -U setuptools
- python3 -m pip install -r requirements.txt
- python3 -m pip install pylint
- python3 -m pip install pytest-cov
- python3 -m pip install twine
- python3 -m pip install sphinx
-
-readme:
- erb -T '-' README.md.erb > README.md
-
-doc: readme
- $(MAKE) -C docs/ html
-
-# TODO upgrade those commands
-# https://packaging.python.org/tutorials/packaging-projects/
-build:
- python3 setup.py sdist
-
-# out of box testing / user acceptance before delivery
-oobt: build
- python3 -m pip install ./${dist}
- python3 oobt/demo.py
-
-check: oobt
- python3 -m twine check ${dist}
-
-release: # check
- python3 -m twine upload ${dist}
-
-# run example only
-# and display output (-s)
-example:
- python3 -m pytest -s "tests/test_example.py::TestExample::test_async"
diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index a9520dd..0000000
--- a/Pipfile
+++ /dev/null
@@ -1,13 +0,0 @@
-[[source]]
-url = "https://pypi.org/simple"
-verify_ssl = true
-name = "pypi"
-
-[packages]
-serpapi = {editable = true, path = "."}
-
-[dev-packages]
-alabaster = "*"
-sphinx = "*"
-pytest = "*"
-black = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index e0db2c3..0000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,517 +0,0 @@
-{
- "_meta": {
- "hash": {
- "sha256": "af8f25e5f407e8511d4991ef1bea16153306344c1b3f417c2b005dce48c36352"
- },
- "pipfile-spec": 6,
- "requires": {},
- "sources": [
- {
- "name": "pypi",
- "url": "https://pypi.org/simple",
- "verify_ssl": true
- }
- ]
- },
- "default": {
- "certifi": {
- "hashes": [
- "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
- "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==2023.7.22"
- },
- "charset-normalizer": {
- "hashes": [
- "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96",
- "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c",
- "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710",
- "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706",
- "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020",
- "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252",
- "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad",
- "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329",
- "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a",
- "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f",
- "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6",
- "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4",
- "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a",
- "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46",
- "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2",
- "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23",
- "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace",
- "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd",
- "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982",
- "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10",
- "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2",
- "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea",
- "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09",
- "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5",
- "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149",
- "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489",
- "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9",
- "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80",
- "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592",
- "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3",
- "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6",
- "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed",
- "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c",
- "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200",
- "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a",
- "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e",
- "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d",
- "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6",
- "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623",
- "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669",
- "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3",
- "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa",
- "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9",
- "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2",
- "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f",
- "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1",
- "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4",
- "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a",
- "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8",
- "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3",
- "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029",
- "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f",
- "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959",
- "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22",
- "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7",
- "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952",
- "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346",
- "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e",
- "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d",
- "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299",
- "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd",
- "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a",
- "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3",
- "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037",
- "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94",
- "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c",
- "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858",
- "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a",
- "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449",
- "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c",
- "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918",
- "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1",
- "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c",
- "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
- "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
- ],
- "markers": "python_full_version >= '3.7.0'",
- "version": "==3.2.0"
- },
- "idna": {
- "hashes": [
- "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
- "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==3.4"
- },
- "pygments": {
- "hashes": [
- "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c",
- "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2.15.1"
- },
- "requests": {
- "hashes": [
- "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
- "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2.31.0"
- },
- "serpapi": {
- "editable": true,
- "path": "."
- },
- "urllib3": {
- "hashes": [
- "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11",
- "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2.0.4"
- }
- },
- "develop": {
- "alabaster": {
- "hashes": [
- "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3",
- "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"
- ],
- "index": "pypi",
- "version": "==0.7.13"
- },
- "babel": {
- "hashes": [
- "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610",
- "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2.12.1"
- },
- "black": {
- "hashes": [
- "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3",
- "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb",
- "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087",
- "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320",
- "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6",
- "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3",
- "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc",
- "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f",
- "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587",
- "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91",
- "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a",
- "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad",
- "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926",
- "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9",
- "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be",
- "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd",
- "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96",
- "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491",
- "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2",
- "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a",
- "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f",
- "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"
- ],
- "index": "pypi",
- "version": "==23.7.0"
- },
- "certifi": {
- "hashes": [
- "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
- "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==2023.7.22"
- },
- "charset-normalizer": {
- "hashes": [
- "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96",
- "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c",
- "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710",
- "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706",
- "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020",
- "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252",
- "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad",
- "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329",
- "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a",
- "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f",
- "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6",
- "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4",
- "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a",
- "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46",
- "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2",
- "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23",
- "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace",
- "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd",
- "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982",
- "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10",
- "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2",
- "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea",
- "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09",
- "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5",
- "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149",
- "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489",
- "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9",
- "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80",
- "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592",
- "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3",
- "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6",
- "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed",
- "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c",
- "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200",
- "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a",
- "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e",
- "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d",
- "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6",
- "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623",
- "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669",
- "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3",
- "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa",
- "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9",
- "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2",
- "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f",
- "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1",
- "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4",
- "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a",
- "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8",
- "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3",
- "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029",
- "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f",
- "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959",
- "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22",
- "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7",
- "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952",
- "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346",
- "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e",
- "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d",
- "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299",
- "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd",
- "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a",
- "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3",
- "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037",
- "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94",
- "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c",
- "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858",
- "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a",
- "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449",
- "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c",
- "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918",
- "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1",
- "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c",
- "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
- "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
- ],
- "markers": "python_full_version >= '3.7.0'",
- "version": "==3.2.0"
- },
- "click": {
- "hashes": [
- "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd",
- "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==8.1.6"
- },
- "docutils": {
- "hashes": [
- "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6",
- "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==0.20.1"
- },
- "idna": {
- "hashes": [
- "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
- "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==3.4"
- },
- "imagesize": {
- "hashes": [
- "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b",
- "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==1.4.1"
- },
- "iniconfig": {
- "hashes": [
- "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
- "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2.0.0"
- },
- "jinja2": {
- "hashes": [
- "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
- "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==3.1.2"
- },
- "markupsafe": {
- "hashes": [
- "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e",
- "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e",
- "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431",
- "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686",
- "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559",
- "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc",
- "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c",
- "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0",
- "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4",
- "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9",
- "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575",
- "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba",
- "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d",
- "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3",
- "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00",
- "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155",
- "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac",
- "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52",
- "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f",
- "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8",
- "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b",
- "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24",
- "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea",
- "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198",
- "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0",
- "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee",
- "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be",
- "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2",
- "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707",
- "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6",
- "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58",
- "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779",
- "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636",
- "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c",
- "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad",
- "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee",
- "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc",
- "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2",
- "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48",
- "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7",
- "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e",
- "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b",
- "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa",
- "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5",
- "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e",
- "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb",
- "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9",
- "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57",
- "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc",
- "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2.1.3"
- },
- "mypy-extensions": {
- "hashes": [
- "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
- "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==1.0.0"
- },
- "packaging": {
- "hashes": [
- "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
- "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==23.1"
- },
- "pathspec": {
- "hashes": [
- "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687",
- "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==0.11.1"
- },
- "platformdirs": {
- "hashes": [
- "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421",
- "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==3.9.1"
- },
- "pluggy": {
- "hashes": [
- "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849",
- "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==1.2.0"
- },
- "pygments": {
- "hashes": [
- "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c",
- "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2.15.1"
- },
- "pytest": {
- "hashes": [
- "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32",
- "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"
- ],
- "index": "pypi",
- "version": "==7.4.0"
- },
- "requests": {
- "hashes": [
- "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
- "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2.31.0"
- },
- "snowballstemmer": {
- "hashes": [
- "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1",
- "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"
- ],
- "version": "==2.2.0"
- },
- "sphinx": {
- "hashes": [
- "sha256:8f336d0221c3beb23006b3164ed1d46db9cebcce9cb41cdb9c5ecd4bcc509be0",
- "sha256:9bdfb5a2b28351d4fdf40a63cd006dbad727f793b243e669fc950d7116c634af"
- ],
- "index": "pypi",
- "version": "==7.1.0"
- },
- "sphinxcontrib-applehelp": {
- "hashes": [
- "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228",
- "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==1.0.4"
- },
- "sphinxcontrib-devhelp": {
- "hashes": [
- "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e",
- "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==1.0.2"
- },
- "sphinxcontrib-htmlhelp": {
- "hashes": [
- "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff",
- "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==2.0.1"
- },
- "sphinxcontrib-jsmath": {
- "hashes": [
- "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
- "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==1.0.1"
- },
- "sphinxcontrib-qthelp": {
- "hashes": [
- "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72",
- "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==1.0.3"
- },
- "sphinxcontrib-serializinghtml": {
- "hashes": [
- "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd",
- "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"
- ],
- "markers": "python_version >= '3.5'",
- "version": "==1.1.5"
- },
- "urllib3": {
- "hashes": [
- "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11",
- "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"
- ],
- "markers": "python_version >= '3.7'",
- "version": "==2.0.4"
- }
- }
-}
diff --git a/README.md b/README.md
index 6d0d1ea..15eb407 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,11 @@
+# SerpApi Python Library & Package
+[](https://pypi.org/project/serpapi) [](https://github.com/serpapi/serpapi-python/actions/workflows/ci.yml)
-
-
SerpApi Python Library & Package
-

+Integrate search data into your AI workflow, RAG / fine-tuning, or Python application using this official wrapper for [SerpApi](https://serpapi.com).
-
- [](https://github.com/serpapi/serpapi-python/actions/workflows/ci.yml)
-
+SerpApi supports Google, Google Maps, Google Shopping, Baidu, Yandex, Yahoo, eBay, App Stores, and [more](https://serpapi.com).
-This repository is the home of the *soon–to–be* official Python API wrapper for [SerpApi](https://serpapi.com). This `serpapi` module allows you to access search data in your Python application.
-
-[SerpApi](https://serpapi.com) supports Google, Google Maps, Google Shopping, Bing, Baidu, Yandex, Yahoo, eBay, App Stores, and more. Check out the [documentation](https://serpapi.com/search-api) for a full list.
-
-## Current Status
-
-This project is under development, and will be released to the public on PyPi soon.
+Query a vast range of data at scale, including web search results, flight schedules, stock market data, news headlines, and [more](https://serpapi.com).
## Installation
@@ -24,367 +15,352 @@ To install the `serpapi` package, simply run the following command:
$ pip install serpapi
```
-Please note that this package is separate from the *soon–to–be* legacy `serpapi` module, which is currently available on PyPi as `google-search-results`.
+Please note that this package is separate from the legacy `serpapi` module, which is available on PyPi as `google-search-results`. This package is maintained by SerpApi, and is the recommended way to access the SerpApi service from Python.
-## Usage
+## Simple Usage
Let's start by searching for Coffee on Google:
-```pycon
->>> import serpapi
->>> s = serpapi.search(q="Coffee", engine="google", location="Austin, Texas", hl="en", gl="us")
+```python
+import os
+import serpapi
+
+client = serpapi.Client(api_key=os.getenv("SERPAPI_KEY"))
+results = client.search({
+ "engine": "google",
+ "q": "coffee"
+})
+
+print(results)
```
-The `s` variable now contains a `SerpResults` object, which acts just like a standard dictionary, with some convenient functions added on top.
+The `results` variable now contains a `SerpResults` object, which acts just like a standard dictionary, with some convenient functions added on top.
-Let's print the first result:
+This example runs a search for "coffee" on Google. It then returns the results as a regular Python Hash.
+ See the [playground](https://serpapi.com/playground) to generate your own code.
-```pycon
->>> s["organic_results"][0]["link"]
-'https://en.wikipedia.org/wiki/Coffee'
-```
+The SerpApi key can be obtained from [serpapi.com/signup](https://serpapi.com/users/sign_up?plan=free).
-Let's print the title of the first result, but in a more Pythonic way:
+Environment variables are a secure, safe, and easy way to manage secrets.
+ Set `export SERPAPI_KEY=` in your shell.
+ Python accesses these variables from `os.environ["SERPAPI_KEY"]`.
-```pycon
->>> s["organic_results"][0].get("title")
-'Coffee - Wikipedia'
-```
+### Error handling
-The [SerpApi.com API Documentation](https://serpapi.com/search-api) contains a list of all the possible parameters that can be passed to the API.
+Unsuccessful requests raise `serpapi.HTTPError` or `serpapi.TimeoutError` exceptions. The returned status code will reflect the sort of error that occurred, please refer to [Status and Error Codes Documentation](https://serpapi.com/api-status-and-error-codes) for more details.
+
+```python
+import os
+import serpapi
+
+# A default timeout can be set here.
+client = serpapi.Client(api_key=os.getenv("API_KEY"), timeout=10)
+
+try:
+ results = client.search({
+ 'engine': 'google',
+ 'q': 'coffee',
+ })
+except serpapi.HTTPError as e:
+ if e.status_code == 401: # Invalid API key
+ print(e.error) # "Invalid API key. Your API key should be here: https://serpapi.com/manage-api-key"
+ elif e.status_code == 400: # Missing required parameter
+ pass
+ elif e.status_code == 429: # Exceeds the hourly throughput limit OR account run out of searches
+ pass
+except serpapi.TimeoutError as e:
+ # Handle timeout
+ print(f"The request timed out: {e}")
+```
## Documentation
Documentation is [available on Read the Docs](https://serpapi-python.readthedocs.io/en/latest/).
-## Examples in python
-Here is how to calls the APIs.
+Change history is [available on GitHub](https://github.com/serpapi/serpapi-python/blob/master/HISTORY.md).
+
+## Basic Examples in Python
-### Search bing
+### Search Bing
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'bing',
- 'q': 'coffee',
+ 'q': 'coffee'
})
```
-test: [tests/example_search_bing_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_bing_test.py)
-see: [serpapi.com/bing-search-api](https://serpapi.com/bing-search-api)
+- API Documentation: [serpapi.com/bing-search-api](https://serpapi.com/bing-search-api)
-### Search baidu
+### Search Baidu
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'baidu',
'q': 'coffee',
})
```
-test: [tests/example_search_baidu_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_baidu_test.py)
-see: [serpapi.com/baidu-search-api](https://serpapi.com/baidu-search-api)
+- API Documentation: [serpapi.com/baidu-search-api](https://serpapi.com/baidu-search-api)
-### Search yahoo
+### Search Yahoo
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'yahoo',
'p': 'coffee',
})
```
-test: [tests/example_search_yahoo_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_yahoo_test.py)
-see: [serpapi.com/yahoo-search-api](https://serpapi.com/yahoo-search-api)
+- API Documentation: [serpapi.com/yahoo-search-api](https://serpapi.com/yahoo-search-api)
-### Search youtube
+### Search YouTube
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'youtube',
'search_query': 'coffee',
})
```
-test: [tests/example_search_youtube_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_youtube_test.py)
-see: [serpapi.com/youtube-search-api](https://serpapi.com/youtube-search-api)
+- API Documentation: [serpapi.com/youtube-search-api](https://serpapi.com/youtube-search-api)
-### Search walmart
+### Search Walmart
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'walmart',
'query': 'coffee',
})
```
-test: [tests/example_search_walmart_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_walmart_test.py)
-see: [serpapi.com/walmart-search-api](https://serpapi.com/walmart-search-api)
+- API Documentation: [serpapi.com/walmart-search-api](https://serpapi.com/walmart-search-api)
-### Search ebay
+### Search eBay
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'ebay',
'_nkw': 'coffee',
})
```
-test: [tests/example_search_ebay_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_ebay_test.py)
-see: [serpapi.com/ebay-search-api](https://serpapi.com/ebay-search-api)
+- API Documentation: [serpapi.com/ebay-search-api](https://serpapi.com/ebay-search-api)
-### Search naver
+### Search Naver
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'naver',
'query': 'coffee',
})
```
-test: [tests/example_search_naver_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_naver_test.py)
-see: [serpapi.com/naver-search-api](https://serpapi.com/naver-search-api)
+- API Documentation: [serpapi.com/naver-search-api](https://serpapi.com/naver-search-api)
-### Search home depot
+### Search Home Depot
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'home_depot',
'q': 'table',
})
```
-test: [tests/example_search_home_depot_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_home_depot_test.py)
-see: [serpapi.com/home-depot-search-api](https://serpapi.com/home-depot-search-api)
+- API Documentation: [serpapi.com/home-depot-search-api](https://serpapi.com/home-depot-search-api)
-### Search apple app store
+### Search Apple App Store
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'apple_app_store',
'term': 'coffee',
})
```
-test: [tests/example_search_apple_app_store_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_apple_app_store_test.py)
-see: [serpapi.com/apple-app-store](https://serpapi.com/apple-app-store)
+- API Documentation: [serpapi.com/apple-app-store](https://serpapi.com/apple-app-store)
-### Search duckduckgo
+### Search DuckDuckGo
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'duckduckgo',
'q': 'coffee',
})
```
-test: [tests/example_search_duckduckgo_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_duckduckgo_test.py)
-see: [serpapi.com/duckduckgo-search-api](https://serpapi.com/duckduckgo-search-api)
+- API Documentation: [serpapi.com/duckduckgo-search-api](https://serpapi.com/duckduckgo-search-api)
-### Search google
+### Search Google
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
- 'engine': 'google',
- 'q': 'coffee',
+results = client.search({
'engine': 'google',
+ 'q': 'coffee'
})
```
-test: [tests/example_search_google_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_google_test.py)
-see: [serpapi.com/search-api](https://serpapi.com/search-api)
+- API Documentation: [serpapi.com/search-api](https://serpapi.com/search-api)
-### Search google scholar
+### Search Google Scholar
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'google_scholar',
'q': 'coffee',
})
```
-test: [tests/example_search_google_scholar_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_google_scholar_test.py)
-see: [serpapi.com/google-scholar-api](https://serpapi.com/google-scholar-api)
+- API Documentation: [serpapi.com/google-scholar-api](https://serpapi.com/google-scholar-api)
-### Search google autocomplete
+### Search Google Autocomplete
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'google_autocomplete',
'q': 'coffee',
})
```
-test: [tests/example_search_google_autocomplete_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_google_autocomplete_test.py)
-see: [serpapi.com/google-autocomplete-api](https://serpapi.com/google-autocomplete-api)
+- API Documentation: [serpapi.com/google-autocomplete-api](https://serpapi.com/google-autocomplete-api)
-### Search google product
+### Search Google Immersive Product
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
- 'engine': 'google_product',
- 'q': 'coffee',
- 'product_id': '4887235756540435899',
+results = client.search({
+ 'engine': 'google_immersive_product',
+ 'page_token': 'eyJlaSI6Im5ZVmxaOXVVTDY2X3A4NFBqTnZELUFjIiwicHJvZHVjdGlkIjoiIiwiY2F0YWxvZ2lkIjoiNTE1NDU2NTc1NTc5MzcxMDY3NSIsImhlYWRsaW5lT2ZmZXJEb2NpZCI6IjI1MDkyMjcwMDUzMjk2NzQwODMiLCJpbWFnZURvY2lkIjoiMTYzOTg5MjU0MDcwMDU4MDA1NTQiLCJyZHMiOiJQQ18zNDg4MDE0MTg3ODgxNzc5NjU0fFBST0RfUENfMzQ4ODAxNDE4Nzg4MTc3OTY1NCIsInF1ZXJ5IjoibGcrdHYiLCJncGNpZCI6IjM0ODgwMTQxODc4ODE3Nzk2NTQiLCJtaWQiOiI1NzY0NjI3ODM3Nzc5MTUzMTMiLCJwdnQiOiJoZyIsInV1bGUiOm51bGx9=',
})
```
-test: [tests/example_search_google_product_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_google_product_test.py)
-see: [serpapi.com/google-product-api](https://serpapi.com/google-product-api)
+- API Documentation: [serpapi.com/google-immersive-product-api](https://serpapi.com/google-immersive-product-api)
-### Search google reverse image
+### Search Google Reverse Image
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'google_reverse_image',
'image_url': 'https://i.imgur.com/5bGzZi7.jpg',
'max_results': '1',
})
```
-test: [tests/example_search_google_reverse_image_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_google_reverse_image_test.py)
-see: [serpapi.com/google-reverse-image](https://serpapi.com/google-reverse-image)
+- API Documentation: [serpapi.com/google-reverse-image](https://serpapi.com/google-reverse-image)
-### Search google events
+### Search Google Events
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'google_events',
- 'q': 'coffee',
+ 'q': 'Events in Austin',
})
```
-test: [tests/example_search_google_events_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_google_events_test.py)
-see: [serpapi.com/google-events-api](https://serpapi.com/google-events-api)
+- API Documentation: [serpapi.com/google-events-api](https://serpapi.com/google-events-api)
-### Search google local services
+### Search Google Local Services
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'google_local_services',
'q': 'electrician',
'data_cid': '6745062158417646970',
})
```
-test: [tests/example_search_google_local_services_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_google_local_services_test.py)
-see: [serpapi.com/google-local-services-api](https://serpapi.com/google-local-services-api)
+- API Documentation: [serpapi.com/google-local-services-api](https://serpapi.com/google-local-services-api)
-### Search google maps
+### Search Google Maps
```python
-import serpapi
-import pprint
import os
+import serpapi
+
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'google_maps',
'q': 'pizza',
'll': '@40.7455096,-74.0083012,15.1z',
'type': 'search',
})
```
-test: [tests/example_search_google_maps_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_google_maps_test.py)
-see: [serpapi.com/google-maps-api](https://serpapi.com/google-maps-api)
+- API Documentation: [serpapi.com/google-maps-api](https://serpapi.com/google-maps-api)
-### Search google jobs
+### Search Google Jobs
```python
-import serpapi
-import pprint
import os
+import serpapi
+
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'google_jobs',
'q': 'coffee',
})
```
-test: [tests/example_search_google_jobs_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_google_jobs_test.py)
-see: [serpapi.com/google-jobs-api](https://serpapi.com/google-jobs-api)
+- API Documentation: [serpapi.com/google-jobs-api](https://serpapi.com/google-jobs-api)
-### Search google play
+### Search Google Play
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
+results = client.search({
'engine': 'google_play',
'q': 'kite',
'store': 'apps',
- 'max_results': '2',
})
```
-test: [tests/example_search_google_play_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_google_play_test.py)
-see: [serpapi.com/google-play-api](https://serpapi.com/google-play-api)
+- API Documentation: [serpapi.com/google-play-api](https://serpapi.com/google-play-api)
-### Search google images
+### Search Google Images
```python
-import serpapi
-import pprint
import os
+import serpapi
client = serpapi.Client(api_key=os.getenv("API_KEY"))
-data = client.search({
- 'engine': 'google_images',
+results = client.search({
'engine': 'google_images',
'tbm': 'isch',
'q': 'coffee',
})
```
-test: [tests/example_search_google_images_test.py](https://github.com/serpapi/serpapi-python/blob/master/tests/example_search_google_images_test.py)
-see: [serpapi.com/images-results](https://serpapi.com/images-results)
-
+- API Documentation: [serpapi.com/google-images-api](https://serpapi.com/google-images-api)
## License
@@ -393,3 +369,15 @@ MIT License.
## Contributing
Bug reports and pull requests are welcome on GitHub. Once dependencies are installed, you can run the tests with `pytest`.
+
+## Publishing a new release
+
+1. Update the version in `serpapi/__version__.py`.
+2. Push a tag — the release pipeline runs automatically:
+ ```sh
+ git tag v1.2.3
+ git push origin v1.2.3
+ ```
+ This triggers the [release workflow](.github/workflows/release.yml), which tests, builds, and publishes to PyPI, then smoke-tests the published package.
+
+> **Required secrets:** `PYPI_API_TOKEN` (PyPI upload token) and `API_KEY` (used in smoke-test live search).
diff --git a/README.md.erb b/README.md.erb
deleted file mode 100644
index 0bed824..0000000
--- a/README.md.erb
+++ /dev/null
@@ -1,165 +0,0 @@
-<%-
-def snippet(format, path)
- lines = File.new(path).readlines
- stop = lines.size - 1
- slice = lines[7..stop]
- slice.reject! { |l| l.match?(/(^# |assert )/) }
- buf = slice.map { |l| l.gsub(/(^\s{2})/, '').gsub(/^\s*$/, '') }.join
- url = 'https://github.com/serpapi/serpapi-python/blob/master/' + path
- %Q(```#{format}\nimport serpapi\nimport pprint\nimport os\n\n#{buf}```\ntest: [#{path}](#{url}))
-end
--%>
-
-
-
SerpApi Python Library & Package
-

-
-
- [](https://github.com/serpapi/serpapi-python/actions/workflows/ci.yml)
-
-
-This repository is the home of the *soon–to–be* official Python API wrapper for [SerpApi](https://serpapi.com). This `serpapi` module allows you to access search data in your Python application.
-
-[SerpApi](https://serpapi.com) supports Google, Google Maps, Google Shopping, Bing, Baidu, Yandex, Yahoo, eBay, App Stores, and more. Check out the [documentation](https://serpapi.com/search-api) for a full list.
-
-## Current Status
-
-This project is under development, and will be released to the public on PyPi soon.
-
-## Installation
-
-To install the `serpapi` package, simply run the following command:
-
-```bash
-$ pip install serpapi
-```
-
-Please note that this package is separate from the *soon–to–be* legacy `serpapi` module, which is currently available on PyPi as `google-search-results`.
-
-## Usage
-
-Let's start by searching for Coffee on Google:
-
-```pycon
->>> import serpapi
->>> s = serpapi.search(q="Coffee", engine="google", location="Austin, Texas", hl="en", gl="us")
-```
-
-The `s` variable now contains a `SerpResults` object, which acts just like a standard dictionary, with some convenient functions added on top.
-
-Let's print the first result:
-
-```pycon
->>> s["organic_results"][0]["link"]
-'https://en.wikipedia.org/wiki/Coffee'
-```
-
-Let's print the title of the first result, but in a more Pythonic way:
-
-```pycon
->>> s["organic_results"][0].get("title")
-'Coffee - Wikipedia'
-```
-
-The [SerpApi.com API Documentation](https://serpapi.com/search-api) contains a list of all the possible parameters that can be passed to the API.
-
-## Documentation
-
-Documentation is [available on Read the Docs](https://serpapi-python.readthedocs.io/en/latest/).
-
-## Examples in python
-Here is how to calls the APIs.
-
-### Search bing
-<%= snippet('python', 'tests/example_search_bing_test.py') %>
-see: [serpapi.com/bing-search-api](https://serpapi.com/bing-search-api)
-
-### Search baidu
-<%= snippet('python', 'tests/example_search_baidu_test.py') %>
-see: [serpapi.com/baidu-search-api](https://serpapi.com/baidu-search-api)
-
-### Search yahoo
-<%= snippet('python', 'tests/example_search_yahoo_test.py') %>
-see: [serpapi.com/yahoo-search-api](https://serpapi.com/yahoo-search-api)
-
-### Search youtube
-<%= snippet('python', 'tests/example_search_youtube_test.py') %>
-see: [serpapi.com/youtube-search-api](https://serpapi.com/youtube-search-api)
-
-### Search walmart
-<%= snippet('python', 'tests/example_search_walmart_test.py') %>
-see: [serpapi.com/walmart-search-api](https://serpapi.com/walmart-search-api)
-
-### Search ebay
-<%= snippet('python', 'tests/example_search_ebay_test.py') %>
-see: [serpapi.com/ebay-search-api](https://serpapi.com/ebay-search-api)
-
-### Search naver
-<%= snippet('python', 'tests/example_search_naver_test.py') %>
-see: [serpapi.com/naver-search-api](https://serpapi.com/naver-search-api)
-
-### Search home depot
-<%= snippet('python', 'tests/example_search_home_depot_test.py') %>
-see: [serpapi.com/home-depot-search-api](https://serpapi.com/home-depot-search-api)
-
-### Search apple app store
-<%= snippet('python', 'tests/example_search_apple_app_store_test.py') %>
-see: [serpapi.com/apple-app-store](https://serpapi.com/apple-app-store)
-
-### Search duckduckgo
-<%= snippet('python', 'tests/example_search_duckduckgo_test.py') %>
-see: [serpapi.com/duckduckgo-search-api](https://serpapi.com/duckduckgo-search-api)
-
-### Search google
-<%= snippet('python', 'tests/example_search_google_test.py') %>
-see: [serpapi.com/search-api](https://serpapi.com/search-api)
-
-### Search google scholar
-<%= snippet('python', 'tests/example_search_google_scholar_test.py') %>
-see: [serpapi.com/google-scholar-api](https://serpapi.com/google-scholar-api)
-
-### Search google autocomplete
-<%= snippet('python', 'tests/example_search_google_autocomplete_test.py') %>
-see: [serpapi.com/google-autocomplete-api](https://serpapi.com/google-autocomplete-api)
-
-### Search google product
-<%= snippet('python', 'tests/example_search_google_product_test.py') %>
-see: [serpapi.com/google-product-api](https://serpapi.com/google-product-api)
-
-### Search google reverse image
-<%= snippet('python', 'tests/example_search_google_reverse_image_test.py') %>
-see: [serpapi.com/google-reverse-image](https://serpapi.com/google-reverse-image)
-
-### Search google events
-<%= snippet('python', 'tests/example_search_google_events_test.py') %>
-see: [serpapi.com/google-events-api](https://serpapi.com/google-events-api)
-
-### Search google local services
-<%= snippet('python', 'tests/example_search_google_local_services_test.py') %>
-see: [serpapi.com/google-local-services-api](https://serpapi.com/google-local-services-api)
-
-### Search google maps
-<%= snippet('python', 'tests/example_search_google_maps_test.py') %>
-see: [serpapi.com/google-maps-api](https://serpapi.com/google-maps-api)
-
-### Search google jobs
-<%= snippet('python', 'tests/example_search_google_jobs_test.py') %>
-see: [serpapi.com/google-jobs-api](https://serpapi.com/google-jobs-api)
-
-### Search google play
-<%= snippet('python', 'tests/example_search_google_play_test.py') %>
-see: [serpapi.com/google-play-api](https://serpapi.com/google-play-api)
-
-### Search google images
-<%= snippet('python', 'tests/example_search_google_images_test.py') %>
-see: [serpapi.com/images-results](https://serpapi.com/images-results)
-
-
-## License
-
-MIT License.
-
-## Contributing
-
-Bug reports and pull requests are welcome on GitHub. Once dependencies are installed, you can run the tests with `pytest`.
diff --git a/docs/index.rst b/docs/index.rst
index 74c9f80..717c69a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,4 +1,4 @@
-.. serapi-python documentation master file, created by
+.. serpapi-python documentation master file, created by
sphinx-quickstart on Sun Apr 3 21:09:40 2022.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
@@ -37,6 +37,11 @@ For example, the API endpoint ``https://serpapi.com/search.json`` is represented
Any parameters that you pass to ``search()`` will be passed to the API. This includes the ``api_key`` parameter, which is required for all requests.
+.. _using-api-client-directly:
+
+Using the API Client directly
+^^^^^^^^^
+
To make this less repetitive, and gain the benefit of connection pooling, let's start using the API Client directly::
>>> client = serpapi.Client(api_key="secret_api_key")
@@ -116,9 +121,11 @@ You can get the next page of results::
>>> type(s.next_page())
-Or iterate over all pages of results::
+To iterate over all pages of results, it's recommended to :ref:`use the API Client directly `::
- >>> for page in s.yield_pages():
+ >>> client = serpapi.Client(api_key="secret_api_key")
+ >>> search = client.search(q="Coffee", engine="google", location="Austin, Texas", hl="en", gl="us")
+ >>> for page in search.yield_pages():
... print(page["search_metadata"]["page_number"])
1
2
diff --git a/pyproject.toml b/pyproject.toml
index b0471b7..0df45a5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,55 @@
[build-system]
-requires = ["setuptools", "wheel"]
-build-backend = "setuptools.build_meta:__legacy__"
\ No newline at end of file
+requires = ["setuptools>=61", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "serpapi"
+dynamic = ["version"]
+description = "The official Python client for SerpApi.com."
+readme = "README.md"
+license = { text = "MIT" }
+authors = [{ name = "SerpApi", email = "support@serpapi.com" }]
+requires-python = ">=3.6"
+dependencies = ["requests"]
+keywords = [
+ "scrape", "serp", "api", "serpapi", "scraping", "json", "search",
+ "localized", "rank", "google", "bing", "baidu", "yandex", "yahoo",
+ "ebay", "scale", "datamining", "training", "machine", "ml",
+ "youtube", "naver", "walmart", "apple", "store", "app",
+]
+classifiers = [
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Natural Language :: English",
+ "Topic :: Utilities",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: Indexing/Search",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+]
+
+[project.optional-dependencies]
+color = ["pygments"]
+test = ["pytest"]
+
+[project.urls]
+Homepage = "https://github.com/serpapi/serpapi-python"
+Source = "https://github.com/serpapi/serpapi-python"
+Documentation = "https://serpapi-python.readthedocs.io/en/latest/"
+
+[tool.setuptools.dynamic]
+version = { attr = "serpapi.__version__.__version__" }
+
+[tool.setuptools.packages.find]
+exclude = ["tests", "tests.*"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
diff --git a/serpapi/__version__.py b/serpapi/__version__.py
index f102a9c..7863915 100644
--- a/serpapi/__version__.py
+++ b/serpapi/__version__.py
@@ -1 +1 @@
-__version__ = "0.0.1"
+__version__ = "1.0.2"
diff --git a/serpapi/core.py b/serpapi/core.py
index a627d21..7d4454a 100644
--- a/serpapi/core.py
+++ b/serpapi/core.py
@@ -25,6 +25,9 @@ class Client(HTTPClient):
DASHBOARD_URL = "https://serpapi.com/dashboard"
+ def __init__(self, *, api_key=None, timeout=None):
+ super().__init__(api_key=api_key, timeout=timeout)
+
def __repr__(self):
return ""
@@ -60,10 +63,16 @@ def search(self, params: dict = None, **kwargs):
if params is None:
params = {}
+ # These are arguments that should be passed to the underlying requests.request call.
+ request_kwargs = {}
+ for key in ["timeout", "proxies", "verify", "stream", "cert"]:
+ if key in kwargs:
+ request_kwargs[key] = kwargs.pop(key)
+
if kwargs:
params.update(kwargs)
- r = self.request("GET", "/search", params=params)
+ r = self.request("GET", "/search", params=params, **request_kwargs)
return SerpResults.from_http_response(r, client=self)
@@ -80,6 +89,12 @@ def search_archive(self, params: dict = None, **kwargs):
if params is None:
params = {}
+ # These are arguments that should be passed to the underlying requests.request call.
+ request_kwargs = {}
+ for key in ["timeout", "proxies", "verify", "stream", "cert"]:
+ if key in kwargs:
+ request_kwargs[key] = kwargs.pop(key)
+
if kwargs:
params.update(kwargs)
@@ -90,7 +105,7 @@ def search_archive(self, params: dict = None, **kwargs):
f"Please provide 'search_id', found here: { self.DASHBOARD_URL }"
)
- r = self.request("GET", f"/searches/{ search_id }", params=params)
+ r = self.request("GET", f"/searches/{ search_id }", params=params, **request_kwargs)
return SerpResults.from_http_response(r, client=self)
def locations(self, params: dict = None, **kwargs):
@@ -106,6 +121,12 @@ def locations(self, params: dict = None, **kwargs):
if params is None:
params = {}
+ # These are arguments that should be passed to the underlying requests.request call.
+ request_kwargs = {}
+ for key in ["timeout", "proxies", "verify", "stream", "cert"]:
+ if key in kwargs:
+ request_kwargs[key] = kwargs.pop(key)
+
if kwargs:
params.update(kwargs)
@@ -114,6 +135,7 @@ def locations(self, params: dict = None, **kwargs):
"/locations.json",
params=params,
assert_200=True,
+ **request_kwargs,
)
return r.json()
@@ -129,10 +151,16 @@ def account(self, params: dict = None, **kwargs):
if params is None:
params = {}
+ # These are arguments that should be passed to the underlying requests.request call.
+ request_kwargs = {}
+ for key in ["timeout", "proxies", "verify", "stream", "cert"]:
+ if key in kwargs:
+ request_kwargs[key] = kwargs.pop(key)
+
if kwargs:
params.update(kwargs)
- r = self.request("GET", "/account.json", params=params, assert_200=True)
+ r = self.request("GET", "/account.json", params=params, assert_200=True, **request_kwargs)
return r.json()
diff --git a/serpapi/exceptions.py b/serpapi/exceptions.py
index f501189..c268c4d 100644
--- a/serpapi/exceptions.py
+++ b/serpapi/exceptions.py
@@ -22,10 +22,30 @@ class SearchIDNotProvided(ValueError, SerpApiError):
class HTTPError(requests.exceptions.HTTPError, SerpApiError):
"""HTTP Error."""
- pass
+ def __init__(self, original_exception):
+ if (isinstance(original_exception, requests.exceptions.HTTPError)):
+ http_error_exception: requests.exceptions.HTTPError = original_exception
+
+ self.status_code = http_error_exception.response.status_code
+ try:
+ self.error = http_error_exception.response.json().get("error", None)
+ except requests.exceptions.JSONDecodeError:
+ self.error = None
+ else:
+ self.status_code = -1
+ self.error = None
+
+ super().__init__(*original_exception.args, response=getattr(original_exception, 'response', None), request=getattr(original_exception, 'request', None))
+
class HTTPConnectionError(HTTPError, requests.exceptions.ConnectionError, SerpApiError):
"""Connection Error."""
pass
+
+
+class TimeoutError(requests.exceptions.Timeout, SerpApiError):
+ """Timeout Error."""
+
+ pass
diff --git a/serpapi/http.py b/serpapi/http.py
index c4f6ed1..16da0da 100644
--- a/serpapi/http.py
+++ b/serpapi/http.py
@@ -3,6 +3,7 @@
from .exceptions import (
HTTPError,
HTTPConnectionError,
+ TimeoutError,
)
from .__version__ import __version__
@@ -13,10 +14,11 @@ class HTTPClient:
BASE_DOMAIN = "https://serpapi.com"
USER_AGENT = f"serpapi-python, v{__version__}"
- def __init__(self, *, api_key=None):
+ def __init__(self, *, api_key=None, timeout=None):
# Used to authenticate requests.
# TODO: do we want to support the environment variable? Seems like a security risk.
self.api_key = api_key
+ self.timeout = timeout
self.session = requests.Session()
def request(self, method, path, params, *, assert_200=True, **kwargs):
@@ -34,12 +36,18 @@ def request(self, method, path, params, *, assert_200=True, **kwargs):
try:
headers = {"User-Agent": self.USER_AGENT}
+ # Use the default timeout if one was provided to the client.
+ if self.timeout and "timeout" not in kwargs:
+ kwargs["timeout"] = self.timeout
+
r = self.session.request(
method=method, url=url, params=params, headers=headers, **kwargs
)
except requests.exceptions.ConnectionError as e:
raise HTTPConnectionError(e)
+ except requests.exceptions.Timeout as e:
+ raise TimeoutError(e)
# Raise an exception if the status code is not 200.
if assert_200:
diff --git a/serpapi/models.py b/serpapi/models.py
index 540d67d..0aa7720 100644
--- a/serpapi/models.py
+++ b/serpapi/models.py
@@ -51,7 +51,7 @@ def next_page_url(self):
serpapi_pagination = self.data.get("serpapi_pagination")
if serpapi_pagination:
- return serpapi_pagination.get("next_link")
+ return serpapi_pagination.get("next")
def next_page(self):
"""Return the next page of results, if any."""
@@ -70,12 +70,16 @@ def yield_pages(self, max_pages=1_000):
"""
current_page_count = 0
-
+
current_page = self
- while current_page.next_page_url and current_page_count < max_pages:
- current_page = current_page.next_page()
- current_page_count += 1
+ while current_page and current_page_count < max_pages:
yield current_page
+ current_page_count += 1
+ if current_page.next_page_url:
+ current_page = current_page.next_page()
+ else:
+ break
+
@classmethod
def from_http_response(cls, r, *, client=None):
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 52236a6..0000000
--- a/setup.py
+++ /dev/null
@@ -1,133 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# Note: To use the 'upload' functionality of this file, you must:
-# $ pipenv install twine --dev
-
-import io
-import os
-import sys
-from shutil import rmtree
-
-from setuptools import find_packages, setup, Command
-
-# Package meta-data.
-NAME = "serpapi"
-DESCRIPTION = "The official Python client for SerpApi.com."
-URL = "https://github.com/serpapi/serpapi-python"
-EMAIL = "kenneth@serpapi.com"
-AUTHOR = "SerpApi.com"
-REQUIRES_PYTHON = ">=3.6.0"
-VERSION = None
-
-# What packages are required for this module to be executed?
-REQUIRED = ["requests"]
-
-# What packages are optional?
-EXTRAS = {"color": ["pygments"], "test": ["pytest"]}
-
-here = os.path.abspath(os.path.dirname(__file__))
-
-# Import the README and use it as the long-description.
-# Note: this will only work if 'README.md' is present in your MANIFEST.in file!
-try:
- with io.open(os.path.join(here, "README.md"), encoding="utf-8") as f:
- long_description = "\n" + f.read()
-except FileNotFoundError:
- long_description = DESCRIPTION
-
-# Load the package's __version__.py module as a dictionary.
-about = {}
-if not VERSION:
- project_slug = NAME.lower().replace("-", "_").replace(" ", "_")
- with open(os.path.join(here, project_slug, "__version__.py")) as f:
- exec(f.read(), about)
-else:
- about["__version__"] = VERSION
-
-
-class UploadCommand(Command):
- """Support setup.py upload."""
-
- description = "Build and publish the package."
- user_options = []
-
- @staticmethod
- def status(s):
- """Prints things in bold."""
- print("\033[1m{0}\033[0m".format(s))
-
- def initialize_options(self):
- pass
-
- def finalize_options(self):
- pass
-
- def run(self):
- try:
- self.status("Removing previous builds…")
- rmtree(os.path.join(here, "dist"))
- except OSError:
- pass
-
- self.status("Building Source and Wheel (universal) distribution…")
- os.system("{0} setup.py sdist bdist_wheel --universal".format(sys.executable))
-
- self.status("Uploading the package to PyPI via Twine…")
- os.system("twine upload dist/*")
-
- self.status("Pushing git tags…")
- os.system("git tag v{0}".format(about["__version__"]))
- os.system("git push --tags")
-
- sys.exit()
-
-
-# Where the magic happens:
-setup(
- name=NAME,
- version=about["__version__"],
- description=DESCRIPTION,
- long_description=long_description,
- long_description_content_type="text/markdown",
- author=AUTHOR,
- author_email=EMAIL,
- python_requires=REQUIRES_PYTHON,
- url=URL,
- packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]),
- # If your package is a single module, use this instead of 'packages':
- # py_modules=['mypackage'],
- # entry_points={
- # 'console_scripts': ['mycli=mymodule:cli'],
- # },
- install_requires=REQUIRED,
- extras_require=EXTRAS,
- include_package_data=True,
- license="MIT",
- project_urls={"Documentation": "https://serpapi.com/search-api"},
- keywords="scrape,serp,api,serpapi,scraping,json,search,localized,rank,google,bing,baidu,yandex,yahoo,ebay,scale,datamining,training,machine,ml,youtube,naver,walmart,apple,store,app,serpapi",
- classifiers=[
- # Trove classifiers
- # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
- "License :: OSI Approved :: MIT License",
- "Programming Language :: Python",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.5",
- "Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Natural Language :: English",
- "Topic :: Utilities",
- "Topic :: Internet :: WWW/HTTP",
- "Topic :: Internet :: WWW/HTTP :: Indexing/Search",
- "Topic :: Software Development :: Libraries :: Python Modules",
- "Programming Language :: Python :: Implementation :: CPython",
- ],
- # $ setup.py publish support.
- cmdclass={
- "upload": UploadCommand,
- },
-)
diff --git a/tests/example_search_apple_app_store_test.py b/tests/example_search_apple_app_store_test.py
index 073f3c4..70bb30b 100644
--- a/tests/example_search_apple_app_store_test.py
+++ b/tests/example_search_apple_app_store_test.py
@@ -3,16 +3,10 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_apple_app_store():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_apple_app_store(client):
data = client.search({
'engine': 'apple_app_store',
'term': 'coffee',
})
assert data.get('error') is None
assert data['organic_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_baidu_test.py b/tests/example_search_baidu_test.py
index 98c6fed..d5a0d52 100644
--- a/tests/example_search_baidu_test.py
+++ b/tests/example_search_baidu_test.py
@@ -3,16 +3,11 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_baidu():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+
+def test_search_baidu(client):
data = client.search({
'engine': 'baidu',
'q': 'coffee',
})
assert data.get('error') is None
assert data['organic_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_bing_test.py b/tests/example_search_bing_test.py
index 62e57b8..36a1c79 100644
--- a/tests/example_search_bing_test.py
+++ b/tests/example_search_bing_test.py
@@ -3,16 +3,10 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_bing():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_bing(client):
data = client.search({
'engine': 'bing',
'q': 'coffee',
})
assert data.get('error') is None
assert data['organic_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_duckduckgo_test.py b/tests/example_search_duckduckgo_test.py
index 01b9600..4a198ab 100644
--- a/tests/example_search_duckduckgo_test.py
+++ b/tests/example_search_duckduckgo_test.py
@@ -3,16 +3,10 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_duckduckgo():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_duckduckgo(client):
data = client.search({
'engine': 'duckduckgo',
'q': 'coffee',
})
assert data.get('error') is None
assert data['organic_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_ebay_test.py b/tests/example_search_ebay_test.py
index d50eec3..fd41037 100644
--- a/tests/example_search_ebay_test.py
+++ b/tests/example_search_ebay_test.py
@@ -3,16 +3,10 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_ebay():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_ebay(client):
data = client.search({
'engine': 'ebay',
'_nkw': 'coffee',
})
assert data.get('error') is None
assert data['organic_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_google_autocomplete_test.py b/tests/example_search_google_autocomplete_test.py
index 9a0ab91..3de2012 100644
--- a/tests/example_search_google_autocomplete_test.py
+++ b/tests/example_search_google_autocomplete_test.py
@@ -3,16 +3,10 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_google_autocomplete():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_google_autocomplete(client):
data = client.search({
'engine': 'google_autocomplete',
'q': 'coffee',
})
assert data.get('error') is None
assert data['suggestions']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_google_events_test.py b/tests/example_search_google_events_test.py
index eac2ca6..a46f5bf 100644
--- a/tests/example_search_google_events_test.py
+++ b/tests/example_search_google_events_test.py
@@ -3,16 +3,10 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_google_events():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_google_events(client):
data = client.search({
'engine': 'google_events',
'q': 'coffee',
})
assert data.get('error') is None
assert data['events_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_google_images_test.py b/tests/example_search_google_images_test.py
index 11e61e0..338e69b 100644
--- a/tests/example_search_google_images_test.py
+++ b/tests/example_search_google_images_test.py
@@ -3,9 +3,7 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_google_images():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_google_images(client):
data = client.search({
'engine': 'google_images',
'engine': 'google_images',
@@ -14,7 +12,3 @@ def test_search_google_images():
})
assert data.get('error') is None
assert data['images_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_google_jobs_test.py b/tests/example_search_google_jobs_test.py
index 24d60bf..c465589 100644
--- a/tests/example_search_google_jobs_test.py
+++ b/tests/example_search_google_jobs_test.py
@@ -3,16 +3,10 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_google_jobs():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_google_jobs(client):
data = client.search({
'engine': 'google_jobs',
'q': 'coffee',
})
assert data.get('error') is None
assert data['jobs_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_google_local_services_test.py b/tests/example_search_google_local_services_test.py
index 2adefac..964b013 100644
--- a/tests/example_search_google_local_services_test.py
+++ b/tests/example_search_google_local_services_test.py
@@ -3,9 +3,8 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_google_local_services():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_google_local_services(client):
+
data = client.search({
'engine': 'google_local_services',
'q': 'electrician',
@@ -13,7 +12,3 @@ def test_search_google_local_services():
})
assert data.get('error') is None
assert data['local_ads']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_google_maps_test.py b/tests/example_search_google_maps_test.py
index 80230e9..453b5dc 100644
--- a/tests/example_search_google_maps_test.py
+++ b/tests/example_search_google_maps_test.py
@@ -3,9 +3,7 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_google_maps():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_google_maps(client):
data = client.search({
'engine': 'google_maps',
'q': 'pizza',
@@ -14,7 +12,3 @@ def test_search_google_maps():
})
assert data.get('error') is None
assert data['local_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_google_play_test.py b/tests/example_search_google_play_test.py
index 0327e80..bd44bef 100644
--- a/tests/example_search_google_play_test.py
+++ b/tests/example_search_google_play_test.py
@@ -3,9 +3,7 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_google_play():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_google_play(client):
data = client.search({
'engine': 'google_play',
'q': 'kite',
@@ -14,7 +12,3 @@ def test_search_google_play():
})
assert data.get('error') is None
assert data['organic_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_google_product_test.py b/tests/example_search_google_product_test.py
deleted file mode 100644
index 7f9ab1e..0000000
--- a/tests/example_search_google_product_test.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Example: google_product search engine
-import pytest
-import os
-import serpapi
-
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_google_product():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
- data = client.search({
- 'engine': 'google_product',
- 'q': 'coffee',
- 'product_id': '4887235756540435899',
- })
- assert data.get('error') is None
- assert data['product_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_google_reverse_image_test.py b/tests/example_search_google_reverse_image_test.py
deleted file mode 100644
index 42314dd..0000000
--- a/tests/example_search_google_reverse_image_test.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Example: google_reverse_image search engine
-import pytest
-import os
-import serpapi
-
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_google_reverse_image():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
- data = client.search({
- 'engine': 'google_reverse_image',
- 'image_url': 'https://i.imgur.com/5bGzZi7.jpg',
- 'max_results': '1',
- })
- assert data.get('error') is None
- assert data['image_sizes']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_google_scholar_test.py b/tests/example_search_google_scholar_test.py
index 09d58e4..61ff7e0 100644
--- a/tests/example_search_google_scholar_test.py
+++ b/tests/example_search_google_scholar_test.py
@@ -3,16 +3,11 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_google_scholar():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_google_scholar(client):
+
data = client.search({
'engine': 'google_scholar',
'q': 'coffee',
})
assert data.get('error') is None
assert data['organic_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_google_test.py b/tests/example_search_google_test.py
index 489a165..650bcff 100644
--- a/tests/example_search_google_test.py
+++ b/tests/example_search_google_test.py
@@ -3,9 +3,8 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_google():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_google(client):
+
data = client.search({
'engine': 'google',
'q': 'coffee',
@@ -13,7 +12,3 @@ def test_search_google():
})
assert data.get('error') is None
assert data['organic_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_home_depot_test.py b/tests/example_search_home_depot_test.py
index 6aef2e4..a09f4fd 100644
--- a/tests/example_search_home_depot_test.py
+++ b/tests/example_search_home_depot_test.py
@@ -3,16 +3,11 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_home_depot():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_home_depot(client):
+
data = client.search({
'engine': 'home_depot',
'q': 'table',
})
assert data.get('error') is None
assert data['products']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_naver_test.py b/tests/example_search_naver_test.py
index 25e3fb4..fa2bb26 100644
--- a/tests/example_search_naver_test.py
+++ b/tests/example_search_naver_test.py
@@ -3,16 +3,10 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_naver():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_naver(client):
data = client.search({
'engine': 'naver',
'query': 'coffee',
})
assert data.get('error') is None
assert data['ads_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_walmart_test.py b/tests/example_search_walmart_test.py
index f77b73e..24248fa 100644
--- a/tests/example_search_walmart_test.py
+++ b/tests/example_search_walmart_test.py
@@ -3,9 +3,7 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_walmart():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_walmart(client):
data = client.search({
'engine': 'walmart',
'query': 'coffee',
@@ -13,6 +11,3 @@ def test_search_walmart():
assert data.get('error') is None
assert data['organic_results']
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_yahoo_test.py b/tests/example_search_yahoo_test.py
index 539097f..0a5af69 100644
--- a/tests/example_search_yahoo_test.py
+++ b/tests/example_search_yahoo_test.py
@@ -3,16 +3,10 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_yahoo():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_yahoo(client):
data = client.search({
'engine': 'yahoo',
'p': 'coffee',
})
assert data.get('error') is None
assert data['organic_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/example_search_youtube_test.py b/tests/example_search_youtube_test.py
index 8823752..00c3da6 100644
--- a/tests/example_search_youtube_test.py
+++ b/tests/example_search_youtube_test.py
@@ -3,16 +3,11 @@
import os
import serpapi
-@pytest.mark.skipif((os.getenv("API_KEY") == None), reason="no api_key provided")
-def test_search_youtube():
- client = serpapi.Client(api_key=os.getenv("API_KEY"))
+def test_search_youtube(client):
+
data = client.search({
'engine': 'youtube',
'search_query': 'coffee',
})
assert data.get('error') is None
assert data['video_results']
-
-# os.getenv("API_KEY") is your secret API Key
-# copy/paste from [http://serpapi.com/dashboard] to your bash
-# ```export API_KEY="your_secure_api_key"```
\ No newline at end of file
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
new file mode 100644
index 0000000..8ff68f8
--- /dev/null
+++ b/tests/test_exceptions.py
@@ -0,0 +1,17 @@
+from unittest.mock import Mock
+import requests
+import serpapi
+
+
+def test_http_error():
+ """Ensure that an HTTPError has the correct status code and error."""
+ mock_response = Mock()
+ mock_response.status_code = 401
+ mock_response.json.return_value = { "error": "Invalid API key" }
+
+ requests_error = requests.exceptions.HTTPError(response=mock_response, request=Mock())
+ http_error = serpapi.HTTPError(requests_error)
+
+ assert http_error.status_code == 401
+ assert http_error.error == "Invalid API key"
+ assert http_error.response == mock_response
diff --git a/tests/test_integration.py b/tests/test_integration.py
index c0f9a6c..0ec9f78 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -26,8 +26,10 @@ def test_account_without_credentials():
def test_account_with_bad_credentials(invalid_key_client):
"""Ensure that an HTTPError is raised when account is accessed with invalid API Credentials."""
- with pytest.raises(serpapi.HTTPError):
+ with pytest.raises(serpapi.HTTPError) as exc_info:
invalid_key_client.account()
+
+ assert exc_info.value.response.status_code == 401
def test_account_with_credentials(client):
@@ -38,6 +40,14 @@ def test_account_with_credentials(client):
assert isinstance(account, dict)
+def test_search_with_missing_params(client):
+ with pytest.raises(serpapi.HTTPError) as exc_info:
+ client.search({ "q": "" })
+
+ assert exc_info.value.status_code == 400
+ assert "Missing query `q` parameter" in exc_info.value.error
+
+
def test_coffee_search(coffee_search):
assert isinstance(coffee_search, serpapi.SerpResults)
assert hasattr(coffee_search, "__getitem__")
@@ -58,6 +68,9 @@ def test_coffee_search_n_pages(coffee_search):
max_pages = 3
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 page_count == max_pages
diff --git a/tests/test_timeout.py b/tests/test_timeout.py
new file mode 100644
index 0000000..7ce5fbd
--- /dev/null
+++ b/tests/test_timeout.py
@@ -0,0 +1,39 @@
+import pytest
+import requests
+from serpapi import Client
+
+def test_client_timeout_setting():
+ """Test that timeout can be set on the client and is passed to the request."""
+ client = Client(api_key="test_key", timeout=10)
+ assert client.timeout == 10
+
+def test_request_timeout_override(monkeypatch):
+ """Test that timeout can be overridden in the search method."""
+ client = Client(api_key="test_key", timeout=10)
+
+ def mock_request(method, url, params, headers, timeout, **kwargs):
+ assert timeout == 5
+ # Return a mock response object
+ mock_response = requests.Response()
+ mock_response.status_code = 200
+ mock_response._content = b'{"search_metadata": {"id": "123"}}'
+ return mock_response
+
+ monkeypatch.setattr(client.session, "request", mock_request)
+
+ client.search(q="coffee", timeout=5)
+
+def test_request_default_timeout(monkeypatch):
+ """Test that the client's default timeout is used if none is provided in search."""
+ client = Client(api_key="test_key", timeout=10)
+
+ def mock_request(method, url, params, headers, timeout, **kwargs):
+ assert timeout == 10
+ mock_response = requests.Response()
+ mock_response.status_code = 200
+ mock_response._content = b'{"search_metadata": {"id": "123"}}'
+ return mock_response
+
+ monkeypatch.setattr(client.session, "request", mock_request)
+
+ client.search(q="coffee")