Source code for operators.google_place.text_search
# places_operators.py
import time
import typing as T
import requests
from airflow.models import BaseOperator
from airflow.utils.context import Context
PLACES_BASE = "https://places.googleapis.com/v1"
[docs]
class PlacesTextSearchOperator(BaseOperator):
"""
Uses Places API v1 Text Search (new) to return a list of place ids.
"""
template_fields = ("text_query", "max_pages", "page_pause_seconds")
def __init__(
self,
*,
api_key: str,
text_query: str,
max_pages: int = 5,
page_pause_seconds: float = 3.0,
timeout: int = 30,
session: requests.Session | None = None,
**kwargs,
):
super().__init__(**kwargs)
self.api_key = api_key
self.text_query = text_query
self.max_pages = max_pages
self.page_pause_seconds = page_pause_seconds
self.timeout = timeout
self._session = session or requests.Session()
[docs]
def execute(self, context: Context) -> list[str]:
headers = {
"Content-Type": "application/json",
"X-Goog-Api-Key": self.api_key,
"X-Goog-FieldMask": "places.id,nextPageToken",
}
url = f"{PLACES_BASE}/places:searchText"
# build the base payload ONCE (if you add filters later, put them here too)
base_payload: dict[str, T.Any] = {"textQuery": self.text_query}
def post_json(payload):
# small retry with backoff; re-raise the last HTTPError cleanly
delay = max(self.page_pause_seconds, 2.0)
last_err = None
for attempt in range(5):
try:
r = self._session.post(
url, json=payload, headers=headers, timeout=self.timeout
)
r.raise_for_status()
return r.json()
except requests.HTTPError as e:
last_err = e
body = getattr(e.response, "text", "")
self.log.warning(
"POST failed (attempt %d). Status=%s Body=%s",
attempt + 1,
getattr(e.response, "status_code", "?"),
body[:500],
)
time.sleep(delay)
delay = min(delay * 1.8, 8.0)
# after retries, raise the last seen error
raise last_err
place_ids: list[str] = []
seen_ids: set[str] = set()
seen_tokens: set[str] = set()
page = 0
payload = dict(base_payload) # page 1: textQuery only
while True:
self.log.info("Now processing page: %d", page + 1)
body = post_json(payload)
for p in body.get("places", []):
pid = p.get("id")
if pid and pid not in seen_ids:
seen_ids.add(pid)
place_ids.append(pid)
tok = body.get("nextPageToken")
page += 1
if not tok or page >= self.max_pages:
break
if tok in seen_tokens:
self.log.warning("Duplicate page token seen; stopping pagination.")
break
seen_tokens.add(tok)
# let token become valid, then request NEXT page
time.sleep(max(self.page_pause_seconds, 3.0))
# IMPORTANT: subsequent pages must include the SAME params as page 1 + pageToken
payload = {**base_payload, "pageToken": tok}
self.log.info(
"Collected %d unique place ids across %d page(s).", len(place_ids), page
)
return place_ids