diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bffa656..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", "3.12"] + 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 88404b3..fc96f64 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,24 @@ Release History =============== +1.0.1 (2026-03-18) +------------------ + +- Fix release workflow: YAML syntax error in smoke test step and remove broken GitHub Packages publish. + +1.0.0 (2026-03-18) +------------------ + +- 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) ------------------ diff --git a/Pipfile b/Pipfile deleted file mode 100644 index a5c014f..0000000 --- a/Pipfile +++ /dev/null @@ -1,14 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -serpapi = {editable = true, path = "."} -pytest = "*" - -[dev-packages] -alabaster = "*" -sphinx = "*" -pytest = "*" -black = "*" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index b648257..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,591 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "89b1875c3363b3d6f5d499c666e43404b5260933fb44f45d06e40277eceb34cb" - }, - "pipfile-spec": 6, - "requires": {}, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", - "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" - ], - "markers": "python_version >= '3.6'", - "version": "==2023.11.17" - }, - "charset-normalizer": { - "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" - }, - "idna": { - "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" - ], - "markers": "python_version >= '3.5'", - "version": "==3.4" - }, - "iniconfig": { - "hashes": [ - "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", - "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, - "packaging": { - "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" - ], - "markers": "python_version >= '3.7'", - "version": "==23.2" - }, - "pluggy": { - "hashes": [ - "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", - "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" - ], - "markers": "python_version >= '3.8'", - "version": "==1.3.0" - }, - "pytest": { - "hashes": [ - "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", - "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==7.4.3" - }, - "requests": { - "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" - ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" - }, - "serpapi": { - "editable": true, - "markers": "python_full_version >= '3.6.0'", - "path": "." - }, - "urllib3": { - "hashes": [ - "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", - "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" - ], - "markers": "python_version >= '3.8'", - "version": "==2.1.0" - } - }, - "develop": { - "alabaster": { - "hashes": [ - "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", - "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2" - ], - "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==0.7.13" - }, - "babel": { - "hashes": [ - "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900", - "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed" - ], - "markers": "python_version >= '3.7'", - "version": "==2.13.1" - }, - "black": { - "hashes": [ - "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4", - "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b", - "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f", - "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07", - "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187", - "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6", - "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05", - "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06", - "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e", - "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5", - "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244", - "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f", - "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221", - "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055", - "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479", - "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394", - "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911", - "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==23.11.0" - }, - "certifi": { - "hashes": [ - "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", - "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" - ], - "markers": "python_version >= '3.6'", - "version": "==2023.11.17" - }, - "charset-normalizer": { - "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" - }, - "click": { - "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.7" - }, - "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:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", - "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", - "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", - "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", - "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", - "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", - "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", - "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", - "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", - "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", - "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", - "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", - "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", - "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", - "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", - "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", - "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", - "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", - "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", - "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", - "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", - "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", - "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", - "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", - "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", - "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", - "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", - "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", - "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", - "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", - "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", - "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", - "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", - "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", - "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", - "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:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", - "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2", - "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11" - ], - "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:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" - ], - "markers": "python_version >= '3.7'", - "version": "==23.2" - }, - "pathspec": { - "hashes": [ - "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", - "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" - ], - "markers": "python_version >= '3.7'", - "version": "==0.11.2" - }, - "platformdirs": { - "hashes": [ - "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", - "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731" - ], - "markers": "python_version >= '3.7'", - "version": "==4.0.0" - }, - "pluggy": { - "hashes": [ - "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", - "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" - ], - "markers": "python_version >= '3.8'", - "version": "==1.3.0" - }, - "pygments": { - "hashes": [ - "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", - "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" - ], - "markers": "python_version >= '3.7'", - "version": "==2.17.2" - }, - "pytest": { - "hashes": [ - "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", - "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==7.4.3" - }, - "requests": { - "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" - ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" - }, - "setuptools": { - "hashes": [ - "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2", - "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6" - ], - "markers": "python_version >= '3.12'", - "version": "==69.0.2" - }, - "snowballstemmer": { - "hashes": [ - "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", - "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" - ], - "version": "==2.2.0" - }, - "sphinx": { - "hashes": [ - "sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560", - "sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==7.2.6" - }, - "sphinxcontrib-applehelp": { - "hashes": [ - "sha256:094c4d56209d1734e7d252f6e0b3ccc090bd52ee56807a5d9315b19c122ab15d", - "sha256:39fdc8d762d33b01a7d8f026a3b7d71563ea3b72787d5f00ad8465bd9d6dfbfa" - ], - "markers": "python_version >= '3.9'", - "version": "==1.0.7" - }, - "sphinxcontrib-devhelp": { - "hashes": [ - "sha256:63b41e0d38207ca40ebbeabcf4d8e51f76c03e78cd61abe118cf4435c73d4212", - "sha256:fe8009aed765188f08fcaadbb3ea0d90ce8ae2d76710b7e29ea7d047177dae2f" - ], - "markers": "python_version >= '3.9'", - "version": "==1.0.5" - }, - "sphinxcontrib-htmlhelp": { - "hashes": [ - "sha256:6c26a118a05b76000738429b724a0568dbde5b72391a688577da08f11891092a", - "sha256:8001661c077a73c29beaf4a79968d0726103c5605e27db92b9ebed8bab1359e9" - ], - "markers": "python_version >= '3.9'", - "version": "==2.0.4" - }, - "sphinxcontrib-jsmath": { - "hashes": [ - "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", - "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.1" - }, - "sphinxcontrib-qthelp": { - "hashes": [ - "sha256:62b9d1a186ab7f5ee3356d906f648cacb7a6bdb94d201ee7adf26db55092982d", - "sha256:bf76886ee7470b934e363da7a954ea2825650013d367728588732c7350f49ea4" - ], - "markers": "python_version >= '3.9'", - "version": "==1.0.6" - }, - "sphinxcontrib-serializinghtml": { - "hashes": [ - "sha256:0c64ff898339e1fac29abd2bf5f11078f3ec413cfe9c046d3120d7ca65530b54", - "sha256:9b36e503703ff04f20e9675771df105e58aa029cfcbc23b8ed716019b7416ae1" - ], - "markers": "python_version >= '3.9'", - "version": "==1.1.9" - }, - "urllib3": { - "hashes": [ - "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", - "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" - ], - "markers": "python_version >= '3.8'", - "version": "==2.1.0" - } - } -} diff --git a/README.md b/README.md index 0abfb2d..15eb407 100644 --- a/README.md +++ b/README.md @@ -1,17 +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). - ![Package](https://badge.fury.io/py/serpapi.svg) - - [![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. +SerpApi supports Google, Google Maps, Google Shopping, Baidu, Yandex, Yahoo, eBay, App Stores, and [more](https://serpapi.com). +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 @@ -23,37 +17,68 @@ $ pip install serpapi 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/). +Change history is [available on GitHub](https://github.com/serpapi/serpapi-python/blob/master/HISTORY.md). + ## Basic Examples in Python ### Search Bing @@ -64,7 +89,7 @@ import serpapi client = serpapi.Client(api_key=os.getenv("API_KEY")) results = client.search({ 'engine': 'bing', - 'q': 'coffee', + 'q': 'coffee' }) ``` - API Documentation: [serpapi.com/bing-search-api](https://serpapi.com/bing-search-api) @@ -194,8 +219,7 @@ import serpapi client = serpapi.Client(api_key=os.getenv("API_KEY")) results = client.search({ 'engine': 'google', - 'q': 'coffee', - 'engine': 'google', + 'q': 'coffee' }) ``` - API Documentation: [serpapi.com/search-api](https://serpapi.com/search-api) @@ -226,19 +250,18 @@ results = client.search({ ``` - API Documentation: [serpapi.com/google-autocomplete-api](https://serpapi.com/google-autocomplete-api) -### Search Google Product +### Search Google Immersive Product ```python import os import serpapi client = serpapi.Client(api_key=os.getenv("API_KEY")) results = client.search({ - 'engine': 'google_product', - 'q': 'coffee', - 'product_id': '4887235756540435899', + 'engine': 'google_immersive_product', + 'page_token': 'eyJlaSI6Im5ZVmxaOXVVTDY2X3A4NFBqTnZELUFjIiwicHJvZHVjdGlkIjoiIiwiY2F0YWxvZ2lkIjoiNTE1NDU2NTc1NTc5MzcxMDY3NSIsImhlYWRsaW5lT2ZmZXJEb2NpZCI6IjI1MDkyMjcwMDUzMjk2NzQwODMiLCJpbWFnZURvY2lkIjoiMTYzOTg5MjU0MDcwMDU4MDA1NTQiLCJyZHMiOiJQQ18zNDg4MDE0MTg3ODgxNzc5NjU0fFBST0RfUENfMzQ4ODAxNDE4Nzg4MTc3OTY1NCIsInF1ZXJ5IjoibGcrdHYiLCJncGNpZCI6IjM0ODgwMTQxODc4ODE3Nzk2NTQiLCJtaWQiOiI1NzY0NjI3ODM3Nzc5MTUzMTMiLCJwdnQiOiJoZyIsInV1bGUiOm51bGx9=', }) ``` -- API Documentation: [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 ```python @@ -262,7 +285,7 @@ import serpapi client = serpapi.Client(api_key=os.getenv("API_KEY")) results = client.search({ 'engine': 'google_events', - 'q': 'coffee', + 'q': 'Events in Austin', }) ``` - API Documentation: [serpapi.com/google-events-api](https://serpapi.com/google-events-api) @@ -321,7 +344,6 @@ results = client.search({ 'engine': 'google_play', 'q': 'kite', 'store': 'apps', - 'max_results': '2', }) ``` - API Documentation: [serpapi.com/google-play-api](https://serpapi.com/google-play-api) @@ -338,8 +360,7 @@ results = client.search({ 'q': 'coffee', }) ``` -- API Documentation: [serpapi.com/images-results](https://serpapi.com/images-results) - +- API Documentation: [serpapi.com/google-images-api](https://serpapi.com/google-images-api) ## License @@ -348,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/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 1276d02..7863915 100644 --- a/serpapi/__version__.py +++ b/serpapi/__version__.py @@ -1 +1 @@ -__version__ = "0.1.5" +__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/setup.py b/setup.py deleted file mode 100644 index 085e79a..0000000 --- a/setup.py +++ /dev/null @@ -1,156 +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 TestCommand(Command): - """Support setup.py test.""" - - description = "Test 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): - os.system("{0} -m pytest".format(sys.executable)) - sys.exit() - -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-python.readthedocs.io/en/latest/"}, - 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", - "Programming Language :: Python :: 3.12", - "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, - "test": TestCommand - }, -) diff --git a/tests/example_search_google_product_test.py b/tests/example_search_google_product_test.py deleted file mode 100644 index 9229a07..0000000 --- a/tests/example_search_google_product_test.py +++ /dev/null @@ -1,13 +0,0 @@ -# Example: google_product search engine -import pytest -import os -import serpapi - -def test_search_google_product(client): - data = client.search({ - 'engine': 'google_product', - 'q': 'coffee', - 'product_id': '4887235756540435899', - }) - assert data.get('error') is None - assert data['product_results'] 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 cbe9bac..0000000 --- a/tests/example_search_google_reverse_image_test.py +++ /dev/null @@ -1,13 +0,0 @@ -# Example: google_reverse_image search engine -import pytest -import os -import serpapi - -def test_search_google_reverse_image(client): - 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'] 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 644ef50..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__") 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")