use standard Maps worker jobs API without key

This commit is contained in:
Marco0300
2026-09-05 16:15:03 +02:00
parent 6dd9531a66
commit dbf0ed0999
7 changed files with 128 additions and 34 deletions
+88 -17
View File
@@ -8,7 +8,9 @@ an explicit fallback (DISCOVERY_BACKEND=browser) for local testing.
"""
from __future__ import annotations
import csv
import hashlib
import io
import json
import os
import re
@@ -28,6 +30,10 @@ MAX_RESULTS = 50
GMAPS_POLL_SECONDS = float(os.environ.get("GMAPS_POLL_SECONDS", "2"))
GMAPS_REQUEST_TIMEOUT = int(os.environ.get("GMAPS_REQUEST_TIMEOUT", "300"))
GMAPS_URL = os.environ.get("GMAPS_URL", "").strip().rstrip("/")
# The bundled gosom/google-maps-scraper image exposes a local, unauthenticated
# REST API. The optional SaaS edition uses a different API and requires an
# X-API-Key header; keep support for it only when explicitly selected.
GMAPS_API_MODE = os.environ.get("GMAPS_API_MODE", "standard").strip().lower()
GMAPS_API_KEY = os.environ.get("GMAPS_API_KEY", "").strip()
ALLOWED_REVIEW = {"pending", "approved", "rejected", "contacted"}
_discover_lock = threading.Lock()
@@ -306,24 +312,76 @@ def http_json(method: str, url: str, payload: Any = None, headers: dict[str, str
raise RuntimeError("gmaps_invalid_json") from exc
def http_bytes(method: str, url: str, payload: Any = None, headers: dict[str, str] | None = None, timeout: float = 20) -> bytes:
"""Make a small HTTP request where the response is not JSON (CSV download)."""
data = None if payload is None else json.dumps(payload).encode("utf-8")
request_headers = {"Accept": "*/*", **(headers or {})}
if data is not None:
request_headers["Content-Type"] = "application/json"
request = urllib.request.Request(url, data=data, headers=request_headers, method=method.upper())
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
return response.read()
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace")[:300]
raise RuntimeError(f"gmaps_http_{exc.code}:{detail}") from exc
except urllib.error.URLError as exc:
raise RuntimeError(f"gmaps_unreachable:{exc.reason}") from exc
def gmaps_headers() -> dict[str, str]:
if not GMAPS_API_KEY:
raise RuntimeError("gmaps_api_key_missing")
return {"X-API-Key": GMAPS_API_KEY}
if GMAPS_API_MODE == "saas":
if not GMAPS_API_KEY:
raise RuntimeError("gmaps_api_key_missing_for_saas_mode")
return {"X-API-Key": GMAPS_API_KEY}
return {}
def download_standard_results(job_id: str) -> list[dict[str, Any]]:
"""Download and parse the CSV produced by the standard web worker."""
body = http_bytes(
"GET",
GMAPS_URL + "/api/v1/jobs/" + quote_plus(str(job_id)) + "/download",
headers=gmaps_headers(),
timeout=30,
)
try:
text = body.decode("utf-8-sig")
return [dict(row) for row in csv.DictReader(io.StringIO(text))]
except (UnicodeDecodeError, csv.Error) as exc:
raise RuntimeError("gmaps_csv_invalid") from exc
def submit_gmaps_job(query: str, include_email: bool = False, extra_reviews: bool = False) -> str:
if not GMAPS_URL:
raise RuntimeError("gmaps_not_configured")
payload = {
"keyword": query,
"lang": os.environ.get("GMAPS_LANG", "en"),
"max_depth": max(1, min(5, int(os.environ.get("GMAPS_MAX_DEPTH", "1")))),
"email": bool(include_email),
"extra_reviews": bool(extra_reviews),
"timeout": max(1, min(300, GMAPS_REQUEST_TIMEOUT)),
}
response = http_json("POST", GMAPS_URL + "/api/v1/scrape", payload, gmaps_headers(), timeout=30)
lang = os.environ.get("GMAPS_LANG", "en")
depth = max(1, min(10, int(os.environ.get("GMAPS_MAX_DEPTH", "1"))))
if GMAPS_API_MODE == "saas":
endpoint = "/api/v1/scrape"
payload = {
"keyword": query,
"lang": lang,
"max_depth": depth,
"email": bool(include_email),
"extra_reviews": bool(extra_reviews),
"timeout": max(1, min(300, GMAPS_REQUEST_TIMEOUT)),
}
else:
# Standard gosom web mode uses the local job API. It has no API-key
# authentication and returns a UUID in {"id": ...} with HTTP 201.
endpoint = "/api/v1/jobs"
payload = {
"name": query[:180],
"keywords": [query],
"lang": lang,
"zoom": 15,
"depth": depth,
"email": bool(include_email),
"extra_reviews": bool(extra_reviews),
"max_time": max(60, min(3600, GMAPS_REQUEST_TIMEOUT)),
}
response = http_json("POST", GMAPS_URL + endpoint, payload, gmaps_headers(), timeout=30)
job_id = response.get("job_id") or response.get("id")
if not job_id:
raise RuntimeError("gmaps_job_id_missing")
@@ -334,7 +392,10 @@ def poll_gmaps_job(job_id: str, progress: Callable[[str, int, int], None] | None
if not GMAPS_URL:
raise RuntimeError("gmaps_not_configured")
deadline = time.monotonic() + max(60, GMAPS_REQUEST_TIMEOUT + 60)
terminal_ok = {"completed", "succeeded", "success"}
if GMAPS_API_MODE == "saas":
terminal_ok = {"completed", "succeeded", "success"}
else:
terminal_ok = {"ok", "completed", "succeeded", "success"}
terminal_bad = {"failed", "cancelled", "canceled", "discarded"}
while time.monotonic() < deadline:
response = http_json("GET", GMAPS_URL + "/api/v1/jobs/" + quote_plus(str(job_id)), headers=gmaps_headers(), timeout=30)
@@ -343,7 +404,11 @@ def poll_gmaps_job(job_id: str, progress: Callable[[str, int, int], None] | None
if progress:
progress(status, result_count, 0)
if status in terminal_ok:
return extract_gmaps_results(response.get("results") if "results" in response else response)
if "results" in response:
return extract_gmaps_results(response.get("results"))
if GMAPS_API_MODE == "standard":
return download_standard_results(job_id)
return extract_gmaps_results(response)
if status in terminal_bad:
raise RuntimeError("gmaps_job_" + status + (":" + as_text(response.get("error")) if response.get("error") else ""))
time.sleep(max(0.5, GMAPS_POLL_SECONDS))
@@ -616,14 +681,20 @@ class Handler(BaseHTTPRequestHandler):
conn = db()
try:
if parsed.path == "/api/health":
configured = bool(GMAPS_URL and GMAPS_API_KEY) if backend_mode() == "gmaps" else True
return self.send_json(200, {"status": "ok", "source": backend_mode(), "configured": configured, "maps_service": bool(GMAPS_URL), "maps_api_key": bool(GMAPS_API_KEY)})
configured = bool(GMAPS_URL) if backend_mode() == "gmaps" else True
if GMAPS_API_MODE == "saas":
configured = configured and bool(GMAPS_API_KEY)
return self.send_json(200, {"status": "ok", "source": backend_mode(), "configured": configured, "maps_service": bool(GMAPS_URL), "maps_api_mode": GMAPS_API_MODE, "maps_api_key": bool(GMAPS_API_KEY)})
if parsed.path == "/api/config":
auth_required = GMAPS_API_MODE == "saas"
ready = backend_mode() == "browser" or (bool(GMAPS_URL) and (not auth_required or bool(GMAPS_API_KEY)))
return self.send_json(200, {
"backend": backend_mode(),
"gmaps_url": bool(GMAPS_URL),
"gmaps_api_mode": GMAPS_API_MODE,
"gmaps_auth_required": auth_required,
"gmaps_api_key": bool(GMAPS_API_KEY),
"ready": backend_mode() == "browser" or bool(GMAPS_URL and GMAPS_API_KEY),
"ready": ready,
"email_extraction_default": False,
})
if parsed.path == "/api/prospects":