yelpapi 2.6.0__tar.gz → 2.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yelpapi-2.7.0/.github/workflows/publish.yml +51 -0
- yelpapi-2.7.0/.github/workflows/tests.yml +49 -0
- {yelpapi-2.6.0 → yelpapi-2.7.0}/.gitignore +1 -0
- yelpapi-2.7.0/AGENTS.md +78 -0
- {yelpapi-2.6.0 → yelpapi-2.7.0}/CHANGES.md +7 -0
- {yelpapi-2.6.0 → yelpapi-2.7.0}/PKG-INFO +15 -12
- {yelpapi-2.6.0 → yelpapi-2.7.0}/README.md +7 -7
- {yelpapi-2.6.0 → yelpapi-2.7.0}/pyproject.toml +23 -5
- yelpapi-2.7.0/release-notes.md +6 -0
- {yelpapi-2.6.0 → yelpapi-2.7.0}/tests/test_yelpapi.py +75 -10
- {yelpapi-2.6.0 → yelpapi-2.7.0}/yelpapi/__init__.py +2 -0
- {yelpapi-2.6.0 → yelpapi-2.7.0}/yelpapi/yelpapi.py +8 -5
- yelpapi-2.6.0/.github/workflows/publish.yml +0 -28
- yelpapi-2.6.0/.github/workflows/tests.yml +0 -29
- yelpapi-2.6.0/CLAUDE.md +0 -32
- yelpapi-2.6.0/tests/__init__.py +0 -0
- {yelpapi-2.6.0 → yelpapi-2.7.0}/AUTHORS.md +0 -0
- {yelpapi-2.6.0 → yelpapi-2.7.0}/LICENSE +0 -0
- {yelpapi-2.6.0 → yelpapi-2.7.0}/examples/examples.py +0 -0
- {yelpapi-2.6.0 → yelpapi-2.7.0}/tests/conftest.py +0 -0
- {yelpapi-2.6.0 → yelpapi-2.7.0}/yelpapi/py.typed +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write
|
|
14
|
+
contents: write
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v7
|
|
20
|
+
with:
|
|
21
|
+
python-version: '3.15'
|
|
22
|
+
|
|
23
|
+
- name: Verify tag matches package version
|
|
24
|
+
run: |
|
|
25
|
+
version="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
|
|
26
|
+
if [ "v${version}" != "${{ github.ref_name }}" ]; then
|
|
27
|
+
echo "Tag ${{ github.ref_name }} does not match pyproject.toml version ${version}" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
- name: Extract release notes from CHANGES.md
|
|
32
|
+
run: |
|
|
33
|
+
version="${GITHUB_REF_NAME#v}"
|
|
34
|
+
awk -v v="$version" '/^## /{if (f) exit; f = (index($0, "## " v " ") == 1); next} f' CHANGES.md > release-notes.md
|
|
35
|
+
if [ ! -s release-notes.md ]; then
|
|
36
|
+
echo "No CHANGES.md entry found for ${version}" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
- name: Build
|
|
41
|
+
run: |
|
|
42
|
+
pip install build
|
|
43
|
+
python -m build
|
|
44
|
+
|
|
45
|
+
- name: Publish to PyPI
|
|
46
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
47
|
+
|
|
48
|
+
- name: Create GitHub release
|
|
49
|
+
env:
|
|
50
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
51
|
+
run: gh release create "${{ github.ref_name }}" dist/* --title "YelpAPI ${{ github.ref_name }}" --notes-file release-notes.md
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
typecheck:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v7
|
|
16
|
+
|
|
17
|
+
# mypy targets python_version in pyproject.toml regardless of the interpreter it runs on, so a single job is
|
|
18
|
+
# sufficient. Run it on that same minimum version so installed dependencies and stubs match what users on the
|
|
19
|
+
# minimum get. Keep this equal to python_version.
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v7
|
|
22
|
+
with:
|
|
23
|
+
python-version: '3.11'
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: pip install -e ".[dev]"
|
|
27
|
+
|
|
28
|
+
- name: Type-check with mypy
|
|
29
|
+
run: mypy
|
|
30
|
+
|
|
31
|
+
test:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
strategy:
|
|
34
|
+
matrix:
|
|
35
|
+
python-version: ['3.11', '3.12', '3.13', '3.14', '3.15']
|
|
36
|
+
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v7
|
|
39
|
+
|
|
40
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
41
|
+
uses: actions/setup-python@v7
|
|
42
|
+
with:
|
|
43
|
+
python-version: ${{ matrix.python-version }}
|
|
44
|
+
|
|
45
|
+
- name: Install dependencies
|
|
46
|
+
run: pip install -e ".[dev]"
|
|
47
|
+
|
|
48
|
+
- name: Run tests
|
|
49
|
+
run: pytest
|
yelpapi-2.7.0/AGENTS.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to AI coding agents working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
Install dependencies:
|
|
8
|
+
```
|
|
9
|
+
pip install -e ".[dev]"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Run tests (coverage included by default via `pyproject.toml`):
|
|
13
|
+
```
|
|
14
|
+
pytest
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Run a single test:
|
|
18
|
+
```
|
|
19
|
+
pytest tests/test_yelpapi.py::TestYelpAPI::test_calls_api
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Type-check (strict; configured in `[tool.mypy]` in `pyproject.toml`, which sets the files to check and targets the minimum supported Python):
|
|
23
|
+
```
|
|
24
|
+
mypy
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
CI (`.github/workflows/tests.yml`) runs `pytest` on every supported Python version and `mypy` once in a separate `typecheck` job. Both must pass.
|
|
28
|
+
|
|
29
|
+
## Architecture
|
|
30
|
+
|
|
31
|
+
This is a thin Python wrapper around the [Yelp Fusion API](https://docs.developer.yelp.com/docs/fusion-intro). The entire implementation lives in `yelpapi/yelpapi.py` as a single `YelpAPI` class, re-exported from `yelpapi/__init__.py` via `__all__`.
|
|
32
|
+
|
|
33
|
+
**Design philosophy:** The wrapper is intentionally minimal and extensible — all API parameters are passed through `**kwargs` directly to the HTTP request, so it doesn't break when Yelp adds new parameters. The only thing that would break it is a URL scheme change. Public methods return `dict[str, Any]`; don't introduce `TypedDict`s or response models, since they would break this pass-through design.
|
|
34
|
+
|
|
35
|
+
**Request flow:** Each public method (e.g., `search_query`, `business_query`) validates required parameters, then delegates to `_query()`, which strips `None`-valued kwargs before issuing the GET request via a shared `requests.Session`. `_query()` raises `requests.HTTPError` for HTTP error statuses, and raises `YelpAPI.YelpAPIError` if the JSON body isn't an object or contains an `error` key. Methods that require a location share a `_require_location_or_lat_lng()` helper.
|
|
36
|
+
|
|
37
|
+
**Session management:** A single `requests.Session` is reused for all calls (performance benefit). Users should close it via context manager (`with YelpAPI(...) as api`) or explicitly via `api.close()`.
|
|
38
|
+
|
|
39
|
+
**Tests** use `requests-mock` (via the `mock_request` autouse fixture in `conftest.py`) to intercept HTTP calls, and `Faker` for generating random test data. No real API calls are made in tests.
|
|
40
|
+
|
|
41
|
+
## Conventions
|
|
42
|
+
|
|
43
|
+
- **Typing:** The package ships `py.typed` and must pass strict `mypy`. All library code must be fully annotated. Tests and examples are type-checked but may omit annotations; see the `[[tool.mypy.overrides]]` section of `pyproject.toml`.
|
|
44
|
+
- **Coverage:** Line coverage of `yelpapi/` is 100%. Keep it there; new code paths need tests.
|
|
45
|
+
- **Style:** Use single quotes for strings, and 120-character lines.
|
|
46
|
+
- **Adding an endpoint** requires all of the following:
|
|
47
|
+
- a `*_API_URL` constant
|
|
48
|
+
- a method that validates required parameters and calls `_query()`
|
|
49
|
+
- an entry in the `YelpAPI` class docstring's API list
|
|
50
|
+
- an entry in the README "METHODS" list
|
|
51
|
+
- tests covering both validation failures and success
|
|
52
|
+
|
|
53
|
+
## Supported Python versions
|
|
54
|
+
|
|
55
|
+
Support only Python versions that are currently maintained (not end-of-life); see https://devguide.python.org/versions/. The minimum is currently 3.11. When adding or dropping a version, update all of these together:
|
|
56
|
+
|
|
57
|
+
- `requires-python` and the `Programming Language :: Python :: 3.x` classifiers in `pyproject.toml`
|
|
58
|
+
- `python_version` in `[tool.mypy]` in `pyproject.toml` (keep it equal to the minimum)
|
|
59
|
+
- the `test` job's matrix in `.github/workflows/tests.yml`
|
|
60
|
+
- the Python version used by the `typecheck` job in `tests.yml` (keep it equal to the minimum, matching `python_version`)
|
|
61
|
+
- the Python version used by `publish.yml` (use the newest supported version)
|
|
62
|
+
- the "REQUIREMENTS" section of `README.md`
|
|
63
|
+
- `CHANGES.md`
|
|
64
|
+
|
|
65
|
+
Once the minimum is raised, newly available language and typing features (e.g., `typing.Self` with 3.11) may be used.
|
|
66
|
+
|
|
67
|
+
## Releasing
|
|
68
|
+
|
|
69
|
+
1. Bump `version` in `pyproject.toml`.
|
|
70
|
+
2. Add a `## X.Y.Z (YYYY-MM-DD)` entry at the top of `CHANGES.md`.
|
|
71
|
+
3. Commit, push, and make sure CI is green. The publish workflow does not run tests.
|
|
72
|
+
4. Push an annotated tag:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
git tag -a vX.Y.Z -m "vX.Y.Z" && git push origin vX.Y.Z
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Pushing a `v*` tag triggers `.github/workflows/publish.yml`. Before publishing, it fails if the tag doesn't match the `pyproject.toml` version, or if `CHANGES.md` has no entry for that version. It then builds, publishes to PyPI, and creates a GitHub release whose notes are that version's `CHANGES.md` entry.
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2.7.0 (2026-10-28)
|
|
4
|
+
* Dropped support for end-of-life Python 3.9 and 3.10; added Python 3.15. Minimum supported version is now 3.11.
|
|
5
|
+
* `YelpAPI.__enter__` now returns `Self`, so subclasses are typed correctly when used as context managers.
|
|
6
|
+
* Declared `__all__` in the `yelpapi` package so `from yelpapi import YelpAPI` passes `mypy --strict` (no implicit re-export) and Pyright's private-import checks.
|
|
7
|
+
* Added the `Typing :: Typed` classifier.
|
|
8
|
+
* A JSON response body that isn't an object (e.g., a string or list) now raises `YelpAPI.YelpAPIError` instead of an unhelpful `TypeError` or silently returning the wrong type.
|
|
9
|
+
|
|
3
10
|
## 2.6.0 (2026-03-17)
|
|
4
11
|
* Added 5 new API endpoints: Business Engagement Metrics (`business_engagement_query`), Business Service Offerings (`business_service_offerings_query`), Categories (`categories_query`), Category by Alias (`category_query`), and Review Highlights (`review_highlights_query`).
|
|
5
12
|
* Updated all documentation URLs to the current `docs.developer.yelp.com` reference format.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: yelpapi
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: yelpapi is a pure Python implementation of the Yelp Fusion API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/lanl/yelpapi
|
|
6
6
|
Project-URL: Changelog, https://github.com/lanl/yelpapi/blob/master/CHANGES.md
|
|
@@ -32,21 +32,24 @@ Classifier: Intended Audience :: Developers
|
|
|
32
32
|
Classifier: License :: OSI Approved :: BSD License
|
|
33
33
|
Classifier: Operating System :: OS Independent
|
|
34
34
|
Classifier: Programming Language :: Python :: 3
|
|
35
|
-
Classifier: Programming Language :: Python :: 3
|
|
36
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
35
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
37
36
|
Classifier: Programming Language :: Python :: 3.11
|
|
38
37
|
Classifier: Programming Language :: Python :: 3.12
|
|
39
38
|
Classifier: Programming Language :: Python :: 3.13
|
|
40
39
|
Classifier: Programming Language :: Python :: 3.14
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
41
41
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
42
42
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
43
|
-
|
|
43
|
+
Classifier: Typing :: Typed
|
|
44
|
+
Requires-Python: >=3.11
|
|
44
45
|
Requires-Dist: requests
|
|
45
46
|
Provides-Extra: dev
|
|
46
47
|
Requires-Dist: faker; extra == 'dev'
|
|
48
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
47
49
|
Requires-Dist: pytest; extra == 'dev'
|
|
48
50
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
49
51
|
Requires-Dist: requests-mock; extra == 'dev'
|
|
52
|
+
Requires-Dist: types-requests; extra == 'dev'
|
|
50
53
|
Description-Content-Type: text/markdown
|
|
51
54
|
|
|
52
55
|
# yelpapi
|
|
@@ -54,18 +57,18 @@ Description-Content-Type: text/markdown
|
|
|
54
57
|
[](https://github.com/lanl/yelpapi/actions/workflows/tests.yml)
|
|
55
58
|
|
|
56
59
|
## LICENSE
|
|
57
|
-
This software is licensed under the [BSD 3-Clause License](
|
|
60
|
+
This software is licensed under the [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause). Please refer to the separate [LICENSE](LICENSE) file for the exact text of the license. You are obligated to give attribution if you use this code.
|
|
58
61
|
|
|
59
62
|
## ABOUT
|
|
60
63
|
yelpapi is a pure Python implementation of the [Yelp Fusion API](https://docs.developer.yelp.com/docs/fusion-intro). It is simple, fast, and robust to any changes Yelp may make to the API in the future.
|
|
61
64
|
|
|
62
65
|
## REQUIREMENTS
|
|
63
|
-
This code requires Python 3.
|
|
66
|
+
This code requires Python 3.11 or higher and [requests](https://github.com/psf/requests).
|
|
64
67
|
|
|
65
68
|
## INSTALL
|
|
66
69
|
yelpapi is available on PyPI at https://pypi.org/project/yelpapi/.
|
|
67
70
|
|
|
68
|
-
Install using [pip](
|
|
71
|
+
Install using [pip](https://pip.pypa.io/):
|
|
69
72
|
|
|
70
73
|
pip install yelpapi
|
|
71
74
|
|
|
@@ -75,7 +78,7 @@ This API is demonstrated more thoroughly in [examples.py](examples/examples.py),
|
|
|
75
78
|
```python
|
|
76
79
|
from yelpapi import YelpAPI
|
|
77
80
|
with YelpAPI(api_key) as yelp_api:
|
|
78
|
-
search_results = yelp_api.search_query(
|
|
81
|
+
search_results = yelp_api.search_query(term='ice cream', location='austin, tx')
|
|
79
82
|
```
|
|
80
83
|
|
|
81
84
|
You can also set timeouts so API calls do not block indefinitely in degraded network conditions:
|
|
@@ -83,16 +86,16 @@ You can also set timeouts so API calls do not block indefinitely in degraded net
|
|
|
83
86
|
```python
|
|
84
87
|
from yelpapi import YelpAPI
|
|
85
88
|
with YelpAPI(api_key, timeout_s=3.0) as yelp_api:
|
|
86
|
-
search_results = yelp_api.search_query(
|
|
89
|
+
search_results = yelp_api.search_query(term='ice cream', location='austin, tx')
|
|
87
90
|
```
|
|
88
91
|
|
|
89
92
|
Under the covers, this module uses a [`requests.Session`](https://requests.readthedocs.io/en/latest/user/advanced/#session-objects) object for issuing all API calls, which offers potentially significant performance benefits over issuing separate API calls outside of a session. You should be sure to close the underlying session when all API interactions are complete. The above examples demonstrate using the class as a context manager, which will automatically close the connection when you're done and is the preferred way of using the class, but you can also manually close it like this if a context manager won't work for your use case:
|
|
90
93
|
|
|
91
94
|
```python
|
|
92
95
|
from yelpapi import YelpAPI
|
|
96
|
+
yelp_api = YelpAPI(api_key)
|
|
93
97
|
try:
|
|
94
|
-
yelp_api =
|
|
95
|
-
search_results = yelp_api.search_query(args)
|
|
98
|
+
search_results = yelp_api.search_query(term='ice cream', location='austin, tx')
|
|
96
99
|
finally:
|
|
97
100
|
yelp_api.close()
|
|
98
101
|
```
|
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
[](https://github.com/lanl/yelpapi/actions/workflows/tests.yml)
|
|
4
4
|
|
|
5
5
|
## LICENSE
|
|
6
|
-
This software is licensed under the [BSD 3-Clause License](
|
|
6
|
+
This software is licensed under the [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause). Please refer to the separate [LICENSE](LICENSE) file for the exact text of the license. You are obligated to give attribution if you use this code.
|
|
7
7
|
|
|
8
8
|
## ABOUT
|
|
9
9
|
yelpapi is a pure Python implementation of the [Yelp Fusion API](https://docs.developer.yelp.com/docs/fusion-intro). It is simple, fast, and robust to any changes Yelp may make to the API in the future.
|
|
10
10
|
|
|
11
11
|
## REQUIREMENTS
|
|
12
|
-
This code requires Python 3.
|
|
12
|
+
This code requires Python 3.11 or higher and [requests](https://github.com/psf/requests).
|
|
13
13
|
|
|
14
14
|
## INSTALL
|
|
15
15
|
yelpapi is available on PyPI at https://pypi.org/project/yelpapi/.
|
|
16
16
|
|
|
17
|
-
Install using [pip](
|
|
17
|
+
Install using [pip](https://pip.pypa.io/):
|
|
18
18
|
|
|
19
19
|
pip install yelpapi
|
|
20
20
|
|
|
@@ -24,7 +24,7 @@ This API is demonstrated more thoroughly in [examples.py](examples/examples.py),
|
|
|
24
24
|
```python
|
|
25
25
|
from yelpapi import YelpAPI
|
|
26
26
|
with YelpAPI(api_key) as yelp_api:
|
|
27
|
-
search_results = yelp_api.search_query(
|
|
27
|
+
search_results = yelp_api.search_query(term='ice cream', location='austin, tx')
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
You can also set timeouts so API calls do not block indefinitely in degraded network conditions:
|
|
@@ -32,16 +32,16 @@ You can also set timeouts so API calls do not block indefinitely in degraded net
|
|
|
32
32
|
```python
|
|
33
33
|
from yelpapi import YelpAPI
|
|
34
34
|
with YelpAPI(api_key, timeout_s=3.0) as yelp_api:
|
|
35
|
-
search_results = yelp_api.search_query(
|
|
35
|
+
search_results = yelp_api.search_query(term='ice cream', location='austin, tx')
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Under the covers, this module uses a [`requests.Session`](https://requests.readthedocs.io/en/latest/user/advanced/#session-objects) object for issuing all API calls, which offers potentially significant performance benefits over issuing separate API calls outside of a session. You should be sure to close the underlying session when all API interactions are complete. The above examples demonstrate using the class as a context manager, which will automatically close the connection when you're done and is the preferred way of using the class, but you can also manually close it like this if a context manager won't work for your use case:
|
|
39
39
|
|
|
40
40
|
```python
|
|
41
41
|
from yelpapi import YelpAPI
|
|
42
|
+
yelp_api = YelpAPI(api_key)
|
|
42
43
|
try:
|
|
43
|
-
yelp_api =
|
|
44
|
-
search_results = yelp_api.search_query(args)
|
|
44
|
+
search_results = yelp_api.search_query(term='ice cream', location='austin, tx')
|
|
45
45
|
finally:
|
|
46
46
|
yelp_api.close()
|
|
47
47
|
```
|
|
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "yelpapi"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.7.0"
|
|
8
8
|
description = "yelpapi is a pure Python implementation of the Yelp Fusion API."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {file = "LICENSE"}
|
|
11
11
|
authors = [
|
|
12
12
|
{name = "Geoffrey Fairchild", email = "mail@gfairchild.com"},
|
|
13
13
|
]
|
|
14
|
-
requires-python = ">=3.
|
|
14
|
+
requires-python = ">=3.11"
|
|
15
15
|
dependencies = [
|
|
16
16
|
"requests",
|
|
17
17
|
]
|
|
@@ -21,12 +21,13 @@ classifiers = [
|
|
|
21
21
|
"License :: OSI Approved :: BSD License",
|
|
22
22
|
"Operating System :: OS Independent",
|
|
23
23
|
"Programming Language :: Python :: 3",
|
|
24
|
-
"Programming Language :: Python :: 3.9",
|
|
25
|
-
"Programming Language :: Python :: 3.10",
|
|
26
24
|
"Programming Language :: Python :: 3.11",
|
|
27
25
|
"Programming Language :: Python :: 3.12",
|
|
28
26
|
"Programming Language :: Python :: 3.13",
|
|
29
27
|
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Programming Language :: Python :: 3.15",
|
|
29
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
30
|
+
"Typing :: Typed",
|
|
30
31
|
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
31
32
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
33
|
]
|
|
@@ -34,9 +35,11 @@ classifiers = [
|
|
|
34
35
|
[project.optional-dependencies]
|
|
35
36
|
dev = [
|
|
36
37
|
"faker",
|
|
38
|
+
"mypy",
|
|
37
39
|
"pytest",
|
|
38
40
|
"pytest-cov",
|
|
39
41
|
"requests-mock",
|
|
42
|
+
"types-requests",
|
|
40
43
|
]
|
|
41
44
|
|
|
42
45
|
[project.urls]
|
|
@@ -44,4 +47,19 @@ Homepage = "https://github.com/lanl/yelpapi"
|
|
|
44
47
|
Changelog = "https://github.com/lanl/yelpapi/blob/master/CHANGES.md"
|
|
45
48
|
|
|
46
49
|
[tool.pytest.ini_options]
|
|
47
|
-
addopts = "-vl --cov"
|
|
50
|
+
addopts = "-vl --cov=yelpapi --import-mode=importlib"
|
|
51
|
+
|
|
52
|
+
[tool.mypy]
|
|
53
|
+
strict = true
|
|
54
|
+
python_version = "3.11"
|
|
55
|
+
files = ["yelpapi", "tests", "examples"]
|
|
56
|
+
explicit_package_bases = true
|
|
57
|
+
warn_unreachable = true
|
|
58
|
+
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool", "possibly-undefined"]
|
|
59
|
+
|
|
60
|
+
# The test suite and examples are checked, but we don't require every test
|
|
61
|
+
# helper/fixture to carry full type annotations.
|
|
62
|
+
[[tool.mypy.overrides]]
|
|
63
|
+
module = ["tests.*", "examples.*"]
|
|
64
|
+
disallow_untyped_defs = false
|
|
65
|
+
disallow_incomplete_defs = false
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
* Dropped support for end-of-life Python 3.9 and 3.10; added Python 3.15. Minimum supported version is now 3.11.
|
|
2
|
+
* `YelpAPI.__enter__` now returns `Self`, so subclasses are typed correctly when used as context managers.
|
|
3
|
+
* Declared `__all__` in the `yelpapi` package so `from yelpapi import YelpAPI` passes `mypy --strict` (no implicit re-export) and Pyright's private-import checks.
|
|
4
|
+
* Added the `Typing :: Typed` classifier.
|
|
5
|
+
* A JSON response body that isn't an object (e.g., a string or list) now raises `YelpAPI.YelpAPIError` instead of an unhelpful `TypeError` or silently returning the wrong type.
|
|
6
|
+
|
|
@@ -79,7 +79,7 @@ class TestYelpAPI:
|
|
|
79
79
|
url = faker.uri()
|
|
80
80
|
mock_request.get(url, content=bytes(faker.binary(length=256)))
|
|
81
81
|
|
|
82
|
-
with pytest.raises(
|
|
82
|
+
with pytest.raises(requests.exceptions.JSONDecodeError):
|
|
83
83
|
yelp._query(url)
|
|
84
84
|
|
|
85
85
|
def test_raises_yelp_api_error(self, yelp, faker, mock_request, random_dict):
|
|
@@ -94,6 +94,21 @@ class TestYelpAPI:
|
|
|
94
94
|
|
|
95
95
|
assert exc_info.value.args[0] == f'{error_code}: {error_description}'
|
|
96
96
|
|
|
97
|
+
@pytest.mark.parametrize('body, type_name', [
|
|
98
|
+
('"an error occurred"', 'str'),
|
|
99
|
+
('["error"]', 'list'),
|
|
100
|
+
('42', 'int'),
|
|
101
|
+
('null', 'NoneType'),
|
|
102
|
+
])
|
|
103
|
+
def test_rejects_non_dict_json(self, yelp, faker, mock_request, body, type_name):
|
|
104
|
+
url = faker.uri()
|
|
105
|
+
mock_request.get(url, text=body)
|
|
106
|
+
|
|
107
|
+
with pytest.raises(YelpAPI.YelpAPIError) as exc_info:
|
|
108
|
+
yelp._query(url)
|
|
109
|
+
|
|
110
|
+
assert exc_info.value.args[0] == f'Unexpected response type: {type_name}'
|
|
111
|
+
|
|
97
112
|
@pytest.mark.parametrize('status_code', [400, 401, 403, 404, 500])
|
|
98
113
|
def test_raises_http_error(self, yelp, faker, mock_request, status_code):
|
|
99
114
|
url = faker.uri()
|
|
@@ -114,9 +129,10 @@ class TestYelpAPI:
|
|
|
114
129
|
|
|
115
130
|
|
|
116
131
|
class TestAutocompleteQuery:
|
|
117
|
-
|
|
132
|
+
@pytest.mark.parametrize('invalid_text', [None, ''])
|
|
133
|
+
def test_requires_text(self, yelp, invalid_text):
|
|
118
134
|
with pytest.raises(ValueError):
|
|
119
|
-
yelp.autocomplete_query()
|
|
135
|
+
yelp.autocomplete_query(text=invalid_text)
|
|
120
136
|
|
|
121
137
|
def test_success(self, yelp, faker, mock_request, random_dict):
|
|
122
138
|
mock_request.get(AUTOCOMPLETE_API_URL, json=random_dict)
|
|
@@ -152,6 +168,20 @@ class TestBusinessMatchQuery:
|
|
|
152
168
|
with pytest.raises(ValueError):
|
|
153
169
|
yelp.business_match_query(**params)
|
|
154
170
|
|
|
171
|
+
@pytest.mark.parametrize('empty_param', ['name', 'city', 'state', 'country', 'address1'])
|
|
172
|
+
def test_rejects_empty_params(self, yelp, faker, empty_param):
|
|
173
|
+
params = {
|
|
174
|
+
'name': faker.company(),
|
|
175
|
+
'city': faker.city(),
|
|
176
|
+
'state': faker.state_abbr(),
|
|
177
|
+
'country': faker.country_code(),
|
|
178
|
+
'address1': faker.street_address(),
|
|
179
|
+
}
|
|
180
|
+
params[empty_param] = ''
|
|
181
|
+
|
|
182
|
+
with pytest.raises(ValueError):
|
|
183
|
+
yelp.business_match_query(**params)
|
|
184
|
+
|
|
155
185
|
def test_success(self, yelp, faker, mock_request, random_dict):
|
|
156
186
|
mock_request.get(BUSINESS_MATCH_API_URL, json=random_dict)
|
|
157
187
|
|
|
@@ -165,9 +195,10 @@ class TestBusinessMatchQuery:
|
|
|
165
195
|
|
|
166
196
|
|
|
167
197
|
class TestBusinessEngagementQuery:
|
|
168
|
-
|
|
198
|
+
@pytest.mark.parametrize('invalid_ids', [None, ''])
|
|
199
|
+
def test_requires_business_ids(self, yelp, invalid_ids):
|
|
169
200
|
with pytest.raises(ValueError):
|
|
170
|
-
yelp.business_engagement_query()
|
|
201
|
+
yelp.business_engagement_query(business_ids=invalid_ids)
|
|
171
202
|
|
|
172
203
|
def test_success(self, yelp, faker, mock_request, random_dict):
|
|
173
204
|
mock_request.get(BUSINESS_ENGAGEMENT_API_URL, json=random_dict)
|
|
@@ -233,16 +264,27 @@ class TestFeaturedEventQuery:
|
|
|
233
264
|
with pytest.raises(ValueError):
|
|
234
265
|
yelp.featured_event_query()
|
|
235
266
|
|
|
236
|
-
|
|
267
|
+
@pytest.mark.parametrize('partial_kwargs', [{'latitude': 37.7749}, {'longitude': -122.4194}])
|
|
268
|
+
def test_rejects_partial_lat_lng(self, yelp, partial_kwargs):
|
|
269
|
+
with pytest.raises(ValueError):
|
|
270
|
+
yelp.featured_event_query(**partial_kwargs)
|
|
271
|
+
|
|
272
|
+
def test_success_with_location(self, yelp, faker, mock_request, random_dict):
|
|
237
273
|
mock_request.get(FEATURED_EVENT_API_URL, json=random_dict)
|
|
238
274
|
|
|
239
275
|
assert yelp.featured_event_query(location=faker.city()) == random_dict
|
|
240
276
|
|
|
277
|
+
def test_success_with_lat_lng(self, yelp, faker, mock_request, random_dict):
|
|
278
|
+
mock_request.get(FEATURED_EVENT_API_URL, json=random_dict)
|
|
279
|
+
|
|
280
|
+
assert yelp.featured_event_query(latitude=faker.latitude(), longitude=faker.longitude()) == random_dict
|
|
281
|
+
|
|
241
282
|
|
|
242
283
|
class TestPhoneSearchQuery:
|
|
243
|
-
|
|
284
|
+
@pytest.mark.parametrize('invalid_phone', [None, ''])
|
|
285
|
+
def test_requires_phone(self, yelp, invalid_phone):
|
|
244
286
|
with pytest.raises(ValueError):
|
|
245
|
-
yelp.phone_search_query()
|
|
287
|
+
yelp.phone_search_query(phone=invalid_phone)
|
|
246
288
|
|
|
247
289
|
def test_success(self, yelp, faker, mock_request, random_dict):
|
|
248
290
|
mock_request.get(PHONE_SEARCH_API_URL, json=random_dict)
|
|
@@ -281,7 +323,12 @@ class TestSearchQuery:
|
|
|
281
323
|
with pytest.raises(ValueError):
|
|
282
324
|
yelp.search_query()
|
|
283
325
|
|
|
284
|
-
|
|
326
|
+
@pytest.mark.parametrize('partial_kwargs', [{'latitude': 37.7749}, {'longitude': -122.4194}])
|
|
327
|
+
def test_rejects_partial_lat_lng(self, yelp, partial_kwargs):
|
|
328
|
+
with pytest.raises(ValueError):
|
|
329
|
+
yelp.search_query(**partial_kwargs)
|
|
330
|
+
|
|
331
|
+
def test_success_with_location(self, yelp, faker, mock_request, random_dict):
|
|
285
332
|
mock_call = mock_request.get(SEARCH_API_URL, json=random_dict)
|
|
286
333
|
term = faker.word()
|
|
287
334
|
location = f'{faker.city()}, {faker.state_abbr()}'
|
|
@@ -297,6 +344,11 @@ class TestSearchQuery:
|
|
|
297
344
|
'limit': [str(limit)],
|
|
298
345
|
}
|
|
299
346
|
|
|
347
|
+
def test_success_with_lat_lng(self, yelp, faker, mock_request, random_dict):
|
|
348
|
+
mock_request.get(SEARCH_API_URL, json=random_dict)
|
|
349
|
+
|
|
350
|
+
assert yelp.search_query(latitude=faker.latitude(), longitude=faker.longitude()) == random_dict
|
|
351
|
+
|
|
300
352
|
|
|
301
353
|
class TestTransactionSearchQuery:
|
|
302
354
|
@pytest.mark.parametrize('invalid_type', [None, ''])
|
|
@@ -308,8 +360,21 @@ class TestTransactionSearchQuery:
|
|
|
308
360
|
with pytest.raises(ValueError):
|
|
309
361
|
yelp.transaction_search_query(faker.word())
|
|
310
362
|
|
|
311
|
-
|
|
363
|
+
@pytest.mark.parametrize('partial_kwargs', [{'latitude': 37.7749}, {'longitude': -122.4194}])
|
|
364
|
+
def test_rejects_partial_lat_lng(self, yelp, faker, partial_kwargs):
|
|
365
|
+
with pytest.raises(ValueError):
|
|
366
|
+
yelp.transaction_search_query(faker.word(), **partial_kwargs)
|
|
367
|
+
|
|
368
|
+
def test_success_with_location(self, yelp, faker, mock_request, random_dict):
|
|
312
369
|
transaction_type = faker.word()
|
|
313
370
|
mock_request.get(TRANSACTION_SEARCH_API_URL.format(transaction_type), json=random_dict)
|
|
314
371
|
|
|
315
372
|
assert yelp.transaction_search_query(transaction_type, location=faker.city()) == random_dict
|
|
373
|
+
|
|
374
|
+
def test_success_with_lat_lng(self, yelp, faker, mock_request, random_dict):
|
|
375
|
+
transaction_type = faker.word()
|
|
376
|
+
mock_request.get(TRANSACTION_SEARCH_API_URL.format(transaction_type), json=random_dict)
|
|
377
|
+
|
|
378
|
+
assert yelp.transaction_search_query(
|
|
379
|
+
transaction_type, latitude=faker.latitude(), longitude=faker.longitude()
|
|
380
|
+
) == random_dict
|
|
@@ -21,10 +21,8 @@
|
|
|
21
21
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
from __future__ import annotations
|
|
25
|
-
|
|
26
24
|
from types import TracebackType
|
|
27
|
-
from typing import Any
|
|
25
|
+
from typing import Any, Self
|
|
28
26
|
|
|
29
27
|
import requests
|
|
30
28
|
|
|
@@ -78,7 +76,7 @@ class YelpAPI:
|
|
|
78
76
|
|
|
79
77
|
This class will create and use a single `requests.Session` object for all API calls, which will provide a nice
|
|
80
78
|
performance boost with many calls. To avoid keeping unnecessary connections open, you should be sure to close
|
|
81
|
-
the Session once all Yelp API interactions are complete. This can be done
|
|
79
|
+
the Session once all Yelp API interactions are complete. This can be done manually by calling close() or by
|
|
82
80
|
using it as a context manager.
|
|
83
81
|
"""
|
|
84
82
|
|
|
@@ -112,7 +110,7 @@ class YelpAPI:
|
|
|
112
110
|
"""
|
|
113
111
|
self._yelp_session.close()
|
|
114
112
|
|
|
115
|
-
def __enter__(self) ->
|
|
113
|
+
def __enter__(self) -> Self:
|
|
116
114
|
return self
|
|
117
115
|
|
|
118
116
|
def __exit__(
|
|
@@ -376,6 +374,11 @@ class YelpAPI:
|
|
|
376
374
|
|
|
377
375
|
response_json = response.json()
|
|
378
376
|
|
|
377
|
+
# Every Yelp Fusion endpoint returns a JSON object. Anything else (e.g., a bare string or list) is unexpected
|
|
378
|
+
# and must be rejected before the error check below, which assumes a dict.
|
|
379
|
+
if not isinstance(response_json, dict):
|
|
380
|
+
raise YelpAPI.YelpAPIError(f'Unexpected response type: {type(response_json).__name__}')
|
|
381
|
+
|
|
379
382
|
# Yelp can return one of many different API errors, so check for one of them.
|
|
380
383
|
# The Yelp Fusion API does not yet have a complete list of errors, but this is on the TODO list; see
|
|
381
384
|
# https://github.com/Yelp/yelp-fusion/issues/95 for more info.
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
name: Publish
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- 'v*'
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
publish:
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
environment: pypi
|
|
12
|
-
permissions:
|
|
13
|
-
id-token: write
|
|
14
|
-
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v4
|
|
17
|
-
|
|
18
|
-
- uses: actions/setup-python@v5
|
|
19
|
-
with:
|
|
20
|
-
python-version: '3.13'
|
|
21
|
-
|
|
22
|
-
- name: Build
|
|
23
|
-
run: |
|
|
24
|
-
pip install build
|
|
25
|
-
python -m build
|
|
26
|
-
|
|
27
|
-
- name: Publish to PyPI
|
|
28
|
-
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
name: Tests
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
pull_request:
|
|
6
|
-
|
|
7
|
-
permissions:
|
|
8
|
-
contents: read
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
test:
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
strategy:
|
|
14
|
-
matrix:
|
|
15
|
-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
|
|
16
|
-
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v4
|
|
19
|
-
|
|
20
|
-
- name: Set up Python ${{ matrix.python-version }}
|
|
21
|
-
uses: actions/setup-python@v5
|
|
22
|
-
with:
|
|
23
|
-
python-version: ${{ matrix.python-version }}
|
|
24
|
-
|
|
25
|
-
- name: Install dependencies
|
|
26
|
-
run: pip install ".[dev]"
|
|
27
|
-
|
|
28
|
-
- name: Run tests
|
|
29
|
-
run: pytest
|
yelpapi-2.6.0/CLAUDE.md
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Commands
|
|
6
|
-
|
|
7
|
-
Install dependencies:
|
|
8
|
-
```
|
|
9
|
-
pip install ".[dev]"
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
Run tests (coverage included by default via `pyproject.toml`):
|
|
13
|
-
```
|
|
14
|
-
pytest
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Run a single test:
|
|
18
|
-
```
|
|
19
|
-
pytest tests/test_yelpapi.py::TestYelpAPI::test_calls_api
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Architecture
|
|
23
|
-
|
|
24
|
-
This is a thin Python wrapper around the [Yelp Fusion API](https://docs.developer.yelp.com/docs/fusion-intro). The entire implementation lives in `yelpapi/yelpapi.py` as a single `YelpAPI` class.
|
|
25
|
-
|
|
26
|
-
**Design philosophy:** The wrapper is intentionally minimal and extensible — all API parameters are passed through `**kwargs` directly to the HTTP request, so it doesn't break when Yelp adds new parameters. The only thing that would break it is a URL scheme change.
|
|
27
|
-
|
|
28
|
-
**Request flow:** Each public method (e.g., `search_query`, `business_query`) validates required parameters, then delegates to `_query()`, which strips `None`-valued kwargs before issuing the GET request via a shared `requests.Session`. Errors from Yelp's API (returned as JSON with an `error` key) are raised as `YelpAPI.YelpAPIError`.
|
|
29
|
-
|
|
30
|
-
**Session management:** A single `requests.Session` is reused for all calls (performance benefit). Users should close it via context manager (`with YelpAPI(...) as api`) or explicitly via `api.close()`.
|
|
31
|
-
|
|
32
|
-
**Tests** use `requests-mock` (via the `mock_request` autouse fixture in `conftest.py`) to intercept HTTP calls, and `Faker` for generating random test data. No real API calls are made in tests.
|
yelpapi-2.6.0/tests/__init__.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|