use standard Maps worker jobs API without key
This commit is contained in:
+88
-17
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user