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.
@@ -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
@@ -19,6 +19,7 @@ venv/
19
19
  .pytest_cache/
20
20
  .coverage
21
21
  htmlcov/
22
+ .codebase-memory/
22
23
 
23
24
  # OS
24
25
  .DS_Store
@@ -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.4
1
+ Metadata-Version: 2.5
2
2
  Name: yelpapi
3
- Version: 2.6.0
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.9
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
- Requires-Python: >=3.9
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
  [![Tests](https://github.com/lanl/yelpapi/actions/workflows/tests.yml/badge.svg)](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](http://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.
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.9 or higher and [requests](https://github.com/requests/requests).
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](http://www.pip-installer.org/):
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(args)
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(args)
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 = YelpAPI(api_key)
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
  [![Tests](https://github.com/lanl/yelpapi/actions/workflows/tests.yml/badge.svg)](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](http://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.
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.9 or higher and [requests](https://github.com/requests/requests).
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](http://www.pip-installer.org/):
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(args)
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(args)
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 = YelpAPI(api_key)
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.6.0"
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.9"
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(ValueError):
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
- def test_requires_text(self, yelp):
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
- def test_requires_business_ids(self, yelp):
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
- def test_success(self, yelp, faker, mock_request, random_dict):
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
- def test_requires_phone(self, yelp):
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
- def test_success(self, yelp, faker, mock_request, random_dict):
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
- def test_success(self, yelp, faker, mock_request, random_dict):
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
@@ -22,3 +22,5 @@
22
22
  """
23
23
 
24
24
  from .yelpapi import YelpAPI
25
+
26
+ __all__ = ['YelpAPI']
@@ -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 manully by calling close() or by
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) -> YelpAPI:
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.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes