feat: add GPS nearby locations endpoint with Overpass API #11
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "10-add-gps-nearby-locations-endpoint-with-o"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Add
GET /locations/nearby?lat=X&lng=Y&radius=5000endpoint that queries OpenStreetMap's Overpass API for nearby McDonald's locations, calculates haversine distances, and matches results against the user's saved locations with slot availability info.Changes
src/mcd_tracker_api/services/geo.py: New haversine distance formula (great-circle distance in metres)src/mcd_tracker_api/services/overpass.py: Async Overpass API client using httpx, parses OSM nodes into OsmLocation dataclasses, raises typed exceptions on failuresrc/mcd_tracker_api/routes/nearby.py: The endpoint -- validates lat/lng/radius, calls Overpass, proximity-matches against saved locations (100m threshold), enriches with slot counts, sorts by distancesrc/mcd_tracker_api/schemas.py: Added NearbyLocation, NearbyResponse, SavedLocationMatch schemassrc/mcd_tracker_api/main.py: Registered nearby router before locations router so /locations/nearby matches before /locations/{id}tests/test_geo.py: 5 unit tests for haversine functiontests/test_overpass.py: 14 tests for Overpass client (all HTTP mocked)tests/test_nearby.py: 16 integration tests for the endpoint (Overpass mocked, DB for saved location matching)Test Plan
Review Checklist
Related
Self-Review Findings
Two issues found and fixed in
bdc24fe:Stale docstring in
overpass.py:query_nearbydocstring said "Never raises -- errors are logged and swallowed" but the function actually raisesOverpassTimeoutErrorandOverpassError. Updated to accurately describe the raise behavior.Wrong type hint in
nearby.py:_build_slot_inforeturn type wastuple[int, int, None | str]butnext_reopenis adatetime | Nonefrom the SQLAlchemy query. Fixed totuple[int, int, datetime | None].Both fixes are in the second commit. No logic changes.
PR #11 Review
DOMAIN REVIEW
Tech stack: Python 3.12 / FastAPI / SQLAlchemy / httpx / Pydantic / pytest. Applying Python/FastAPI/SQLAlchemy domain checks.
Haversine formula (
services/geo.py): Mathematically correct. Uses the standardatan2(sqrt(a), sqrt(1-a))form which is numerically stable for both small and large distances. Earth radius constant6_371_000is the accepted mean radius. Tests cover identity, known distance, short range, antipodal, and symmetry -- good coverage.Overpass API client (
services/overpass.py): Properly async viahttpx.AsyncClientin anasync withcontext manager. Timeout handling is correct -- the httpx timeout matches the Overpass QL[timeout:]directive. Error hierarchy (OverpassError>OverpassTimeoutError) is clean. Thebrand="McDonald's"filter is the correct OSM tag. Apostrophe escaping in the f-string is correct.Saved location matching (
routes/nearby.py): The_find_saved_matchfunction correctly finds the closest match under the 100m threshold using<(not<=), and initializesbest_distanceto the threshold itself so only closer matches win. The null lat/lng guard on saved locations is present both in the DB query (lines 112-113) and in the matching loop (lines 40-41), which is belt-and-suspenders -- good.User isolation (
routes/nearby.py, line 111): The saved locations query filters byLocation.keycloak_sub == user.sub, so user A's saved locations cannot leak into user B's results. The_build_slot_infofunction also passesuser.subthrough to the coupon usage query. The isolation is correct at the code level.Route registration (
main.py): The nearby router is registered before the locations router (line 41 vs 42), ensuring/locations/nearbymatches before/locations/{id}. This is correct and the PR body explains the reasoning.Input validation: FastAPI
Queryparameters enforcelat[-90, 90],lng[-180, 180],radius[100, 50000]. All user-supplied values validated before reaching Overpass or the DB.Schemas (
schemas.py):NearbyLocation,NearbyResponse,SavedLocationMatchare clean Pydantic models. Nofrom_attributesneeded since they are constructed manually (not from ORM instances).BLOCKERS
1.
anyio/pytest-anyiomissing from dev dependencies -- async tests will not run in CItests/test_overpass.pyuses@pytest.mark.anyioon 5 async tests (lines 78, 120, 137, 149, 166), but neitheranyionorpytest-anyioappears inpyproject.toml's[project.optional-dependencies] devlist. Without the pytest-anyio plugin installed, these markers will either:--strict-markersis off), orEither way, the 5 async Overpass client tests will not actually execute in CI. The CI step runs
pip install .[dev]followed bypytest, and without the anyio plugin, those tests are dead code.Fix: Add
"anyio[trio]>=4.0"or"pytest-anyio>=0.0.1"(or alternatively"pytest-asyncio>=0.23"and switch to@pytest.mark.asyncio) to thedevdependencies inpyproject.toml.This is a BLOCKER per the "new functionality with zero test coverage" criterion -- the Overpass client's async behavior is tested, but those tests will not actually run.
NITS
1. DRY:
_build_slot_infoduplicates_count_active_codes/_next_reopenfromlocations.pynearby.pylines 50-76 reimplement the exact same two queries that exist as_count_active_codes(locations.py:74-84) and_next_reopen(locations.py:87-98). Consider extracting these into a shared module (e.g.,services/slots.py) that both route files import. Not blocking since this is not an auth/security path, but it creates divergence risk if slot logic changes.2. Overpass query only fetches
nodeelements -- misseswayandrelationMcDonald'sIn OpenStreetMap, McDonald's locations can be mapped as nodes (points), ways (building outlines), or relations (complex buildings). The query
node["brand"="McDonald's"]only returns nodes. A more complete query would usenwr(node/way/relation) and addout center;to get centroid coordinates for ways/relations. This could miss a meaningful percentage of McDonald's in dense urban areas. Non-blocking since the current approach still works -- just incomplete.3. No user isolation test
While the code correctly filters by
user.sub, there is no test that creates saved locations for a different user and verifies they do not appear in the results. A test like "user B's saved location near the OSM result should not match for user A" would strengthen confidence. The existing tests all useTEST_USER.sub.4.
OVERPASS_URLis hardcodedThe Overpass API URL (
https://overpass-api.de/api/interpreter) is a module-level constant. For production resilience (and testing without mocking), consider making this configurable viaSettingsso it can be pointed at a different Overpass instance or a self-hosted one.5.
_build_slot_infoslot count could go negative if data is inconsistentIf
active > MAX_SLOTSdue to data corruption or a race condition,MAX_SLOTS - activewould be negative. Theavailable_slotsfield inSavedLocationMatchhas noge=0constraint. Considermax(0, MAX_SLOTS - active).SOP COMPLIANCE
10-add-gps-nearby-locations-endpoint-with-oreferences issue #10)plan-mcd-trackerPROCESS OBSERVATIONS
plan-mcd-tracker) per SOP convention.VERDICT: NOT APPROVED
One BLOCKER must be fixed:
pytest-anyio(or equivalent) missing from dev dependencies means the 5 async Overpass client tests are not executing in CI. This effectively means the Overpass client has zero verified test coverage, which meets the "new functionality with zero test coverage" BLOCKER criterion.