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 +[![Package](https://img.shields.io/pypi/v/serpapi?color=green)](https://pypi.org/project/serpapi) [![serpapi-python](https://github.com/serpapi/serpapi-python/actions/workflows/ci.yml/badge.svg)](https://github.com/serpapi/serpapi-python/actions/workflows/ci.yml) -
-

SerpApi Python Library & Package

- serpapi python library logo +Integrate search data into your AI workflow, RAG / fine-tuning, or Python application using this official wrapper for [SerpApi](https://serpapi.com). - - [![serpapi-python](https://github.com/serpapi/serpapi-python/actions/workflows/ci.yml/badge.svg)](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

- serpapi python library logo - - - [![serpapi-python](https://github.com/serpapi/serpapi-python/actions/workflows/ci.yml/badge.svg)](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")