From 21268ab477afea78e8404f240b06f79754ff0195 Mon Sep 17 00:00:00 2001 From: Logan Date: Sun, 21 Jun 2026 14:35:12 -0400 Subject: [PATCH] fix: migrate Places and Routes to new GCP APIs Switch from legacy Places textsearch and Directions APIs (disabled on this project) to Places API (New) and Routes API (New). Both places.py and the assistant's _places_search helper updated. Also fixes uid() recursive self-call in trips page and adds Places API response logging. --- drb-c2-core/app/routers/places.py | 68 ++++++++++++++++++++----------- drb-c2-core/app/routers/trips.py | 34 ++++++++++------ 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/drb-c2-core/app/routers/places.py b/drb-c2-core/app/routers/places.py index 2ce71db..80797cf 100644 --- a/drb-c2-core/app/routers/places.py +++ b/drb-c2-core/app/routers/places.py @@ -5,8 +5,9 @@ from app.internal.logger import logger router = APIRouter(prefix="/places", tags=["places"]) -PLACES_SEARCH_URL = "https://maps.googleapis.com/maps/api/place/textsearch/json" -DIRECTIONS_URL = "https://maps.googleapis.com/maps/api/directions/json" +_PLACES_SEARCH_URL = "https://places.googleapis.com/v1/places:searchText" +_ROUTES_URL = "https://routes.googleapis.com/directions/v2:computeRoutes" +_PLACES_FIELDS = "places.id,places.displayName,places.formattedAddress,places.rating,places.googleMapsUri,places.location" @router.get("/search") @@ -17,9 +18,13 @@ async def search_places(query: str = Query(...), near: str = Query("")): full_query = f"{query} {near}".strip() try: async with httpx.AsyncClient(timeout=10) as client: - r = await client.get( - PLACES_SEARCH_URL, - params={"query": full_query, "key": settings.google_maps_api_key}, + r = await client.post( + _PLACES_SEARCH_URL, + json={"textQuery": full_query}, + headers={ + "X-Goog-Api-Key": settings.google_maps_api_key, + "X-Goog-FieldMask": _PLACES_FIELDS, + }, ) r.raise_for_status() data = r.json() @@ -29,15 +34,15 @@ async def search_places(query: str = Query(...), near: str = Query("")): return [ { - "name": p.get("name"), - "address": p.get("formatted_address"), - "place_id": p.get("place_id"), - "lat": p.get("geometry", {}).get("location", {}).get("lat"), - "lng": p.get("geometry", {}).get("location", {}).get("lng"), - "maps_link": f"https://www.google.com/maps/place/?q=place_id:{p.get('place_id')}", + "name": p.get("displayName", {}).get("text"), + "address": p.get("formattedAddress"), + "place_id": p.get("id"), + "lat": p.get("location", {}).get("latitude"), + "lng": p.get("location", {}).get("longitude"), + "maps_link": p.get("googleMapsUri"), "rating": p.get("rating"), } - for p in data.get("results", [])[:6] + for p in data.get("places", [])[:6] ] @@ -51,13 +56,16 @@ async def get_directions( try: async with httpx.AsyncClient(timeout=10) as client: - r = await client.get( - DIRECTIONS_URL, - params={ - "origin": origin, - "destination": destination, - "mode": "driving", - "key": settings.google_maps_api_key, + r = await client.post( + _ROUTES_URL, + json={ + "origin": {"address": origin}, + "destination": {"address": destination}, + "travelMode": "DRIVE", + }, + headers={ + "X-Goog-Api-Key": settings.google_maps_api_key, + "X-Goog-FieldMask": "routes.duration,routes.distanceMeters", }, ) r.raise_for_status() @@ -70,9 +78,23 @@ async def get_directions( if not routes: return {"duration_text": None, "duration_seconds": None, "distance_text": None} - leg = routes[0]["legs"][0] + route = routes[0] + duration_seconds = int(route.get("duration", "0s").rstrip("s") or 0) + distance_m = route.get("distanceMeters", 0) + + # Format human-readable strings + hours, rem = divmod(duration_seconds, 3600) + mins = rem // 60 + if hours: + duration_text = f"{hours} hr {mins} min" if mins else f"{hours} hr" + else: + duration_text = f"{mins} min" + + miles = distance_m / 1609.34 + distance_text = f"{miles:.1f} mi" + return { - "duration_text": leg["duration"]["text"], - "duration_seconds": leg["duration"]["value"], - "distance_text": leg["distance"]["text"], + "duration_text": duration_text, + "duration_seconds": duration_seconds, + "distance_text": distance_text, } diff --git a/drb-c2-core/app/routers/trips.py b/drb-c2-core/app/routers/trips.py index 2e70597..bd5b430 100644 --- a/drb-c2-core/app/routers/trips.py +++ b/drb-c2-core/app/routers/trips.py @@ -74,30 +74,38 @@ _TOOLS = [ ] +_PLACES_SEARCH_URL = "https://places.googleapis.com/v1/places:searchText" +_PLACES_FIELDS = "places.id,places.displayName,places.formattedAddress,places.rating,places.googleMapsUri" + + async def _places_search(query: str, near: str) -> list[dict]: if not settings.google_maps_api_key: return [] + full_query = f"{query} {near}".strip() try: async with httpx.AsyncClient(timeout=8) as client: - r = await client.get( - "https://maps.googleapis.com/maps/api/place/textsearch/json", - params={"query": f"{query} {near}".strip(), "key": settings.google_maps_api_key}, + r = await client.post( + _PLACES_SEARCH_URL, + json={"textQuery": full_query}, + headers={ + "X-Goog-Api-Key": settings.google_maps_api_key, + "X-Goog-FieldMask": _PLACES_FIELDS, + }, ) data = r.json() - status = data.get("status") - results = data.get("results", []) - logger.info(f"Places search '{query} {near}': status={status}, count={len(results)}") - if status not in ("OK", "ZERO_RESULTS"): - logger.warning(f"Places API error: {status} — {data.get('error_message', '')}") + places = data.get("places", []) + logger.info(f"Places search '{full_query}': count={len(places)}") + if not places and "error" in data: + logger.warning(f"Places API error: {data['error'].get('message', '')}") return [ { - "name": p.get("name"), - "address": p.get("formatted_address"), - "place_id": p.get("place_id"), - "maps_link": f"https://www.google.com/maps/place/?q=place_id:{p.get('place_id')}", + "name": p.get("displayName", {}).get("text"), + "address": p.get("formattedAddress"), + "place_id": p.get("id"), + "maps_link": p.get("googleMapsUri"), "rating": p.get("rating"), } - for p in results[:5] + for p in places[:5] ] except Exception as e: logger.error(f"Places search in assistant failed: {e}")