SysOps: voice-agy-webbuilder-backup — 2026-06-23 10:04 UTC
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
"""Google Places sync for Export Intel (optional — requires GOOGLE_MAPS_API_KEY)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from app.db import fetch_one
|
||||
from app.export_intel_sync import _get_territory, _upsert_contact
|
||||
|
||||
PLACES_URL = "https://maps.googleapis.com/maps/api/place/textsearch/json"
|
||||
DETAILS_URL = "https://maps.googleapis.com/maps/api/place/details/json"
|
||||
|
||||
QUERIES = [
|
||||
"meat wholesaler",
|
||||
"halal meat distributor",
|
||||
"poultry distributor",
|
||||
"frozen food supplier",
|
||||
"contract catering",
|
||||
"food importer",
|
||||
"halal butcher",
|
||||
]
|
||||
|
||||
LARGE_COUNTRIES = frozenset({"US", "CA", "BR", "MX", "AR", "AU", "RU", "CN", "IN", "CD", "DZ", "EG", "IR", "SA"})
|
||||
|
||||
|
||||
def _api_key() -> Optional[str]:
|
||||
return os.getenv("GOOGLE_MAPS_API_KEY") or os.getenv("GOOGLE_PLACES_API_KEY")
|
||||
|
||||
|
||||
def _places_search(query: str, lat: float, lon: float, radius_m: int = 80000) -> list[dict[str, Any]]:
|
||||
key = _api_key()
|
||||
if not key:
|
||||
return []
|
||||
params = {
|
||||
"query": query,
|
||||
"location": f"{lat},{lon}",
|
||||
"radius": radius_m,
|
||||
"key": key,
|
||||
}
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
resp = client.get(PLACES_URL, params=params)
|
||||
if resp.status_code >= 400:
|
||||
return []
|
||||
data = resp.json()
|
||||
if data.get("status") not in ("OK", "ZERO_RESULTS"):
|
||||
return []
|
||||
return data.get("results", [])[:12]
|
||||
|
||||
|
||||
def _place_details(place_id: str) -> dict[str, Any]:
|
||||
key = _api_key()
|
||||
if not key:
|
||||
return {}
|
||||
with httpx.Client(timeout=20.0) as client:
|
||||
resp = client.get(
|
||||
DETAILS_URL,
|
||||
params={
|
||||
"place_id": place_id,
|
||||
"fields": "name,formatted_phone_number,website,formatted_address,geometry,international_phone_number",
|
||||
"key": key,
|
||||
},
|
||||
)
|
||||
if resp.status_code >= 400:
|
||||
return {}
|
||||
return resp.json().get("result", {})
|
||||
|
||||
|
||||
def _bbox_points(bbox: Any, grid: int = 3) -> list[tuple[float, float]]:
|
||||
if isinstance(bbox, str):
|
||||
bbox = json.loads(bbox)
|
||||
if not bbox or len(bbox) != 4:
|
||||
return []
|
||||
south, west, north, east = bbox
|
||||
points: list[tuple[float, float]] = []
|
||||
for i in range(grid):
|
||||
for j in range(grid):
|
||||
lat = south + (north - south) * (i + 0.5) / grid
|
||||
lon = west + (east - west) * (j + 0.5) / grid
|
||||
points.append((lat, lon))
|
||||
return points
|
||||
|
||||
|
||||
def _upsert_place(place: dict[str, Any], details: dict[str, Any], country_iso2: str, territory_code: str, query: str) -> tuple[str, int]:
|
||||
from app.db import execute, execute_returning
|
||||
|
||||
pid = place.get("place_id")
|
||||
name = details.get("name") or place.get("name") or "Unknown"
|
||||
geo = details.get("geometry", {}).get("location", place.get("geometry", {}).get("location", {}))
|
||||
plat = geo.get("lat")
|
||||
plon = geo.get("lng")
|
||||
phone = details.get("formatted_phone_number") or details.get("international_phone_number")
|
||||
website = details.get("website")
|
||||
entity_type = "contract_caterer" if "catering" in query else "distributor"
|
||||
|
||||
existing = fetch_one(
|
||||
"SELECT id FROM export_market_entities WHERE metadata->>'google_place_id' = %s",
|
||||
(pid,),
|
||||
)
|
||||
if existing:
|
||||
execute(
|
||||
"""
|
||||
UPDATE export_market_entities SET
|
||||
phone = COALESCE(%s, phone), website = COALESCE(%s, website),
|
||||
lat = COALESCE(%s, lat), lon = COALESCE(%s, lon),
|
||||
confidence = GREATEST(confidence, 65), updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""",
|
||||
(phone, website, plat, plon, existing["id"]),
|
||||
)
|
||||
entity_id = existing["id"]
|
||||
action = "updated"
|
||||
else:
|
||||
row = execute_returning(
|
||||
"""
|
||||
INSERT INTO export_market_entities (
|
||||
name, entity_type, country_iso2, territory_code, lat, lon,
|
||||
phone, website, google_place_id, source, sources, confidence, metadata
|
||||
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,'google_places','["google_places"]'::jsonb,65,%s)
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
name[:255], entity_type, country_iso2.upper(), territory_code,
|
||||
plat, plon, phone, website, pid,
|
||||
json.dumps({"google_place_id": pid}),
|
||||
),
|
||||
)
|
||||
entity_id = row["id"]
|
||||
action = "inserted"
|
||||
if phone:
|
||||
_upsert_contact(entity_id, None, phone, "google_places", 70, role="sales", name="Google Places")
|
||||
return action, entity_id
|
||||
|
||||
|
||||
def sync_places_country(country_iso2: str) -> dict[str, Any]:
|
||||
territory = _get_territory(country_iso2)
|
||||
if not territory or not territory.get("lat"):
|
||||
return {"skipped": True, "reason": "no territory"}
|
||||
if not _api_key():
|
||||
return {"skipped": True, "reason": "GOOGLE_MAPS_API_KEY not set"}
|
||||
|
||||
territory_code = territory["code"]
|
||||
inserted = updated = 0
|
||||
seen: set[str] = set()
|
||||
|
||||
iso = country_iso2.upper()
|
||||
if iso in LARGE_COUNTRIES and territory.get("bbox"):
|
||||
search_points = _bbox_points(territory["bbox"], grid=3)
|
||||
else:
|
||||
search_points = [(territory["lat"], territory["lon"])]
|
||||
|
||||
radius = 60000 if iso in LARGE_COUNTRIES else 80000
|
||||
|
||||
for lat, lon in search_points:
|
||||
for q in QUERIES:
|
||||
for place in _places_search(q, lat, lon, radius_m=radius):
|
||||
pid = place.get("place_id")
|
||||
if not pid or pid in seen:
|
||||
continue
|
||||
seen.add(pid)
|
||||
details = _place_details(pid)
|
||||
action, _ = _upsert_place(place, details, iso, territory_code, q)
|
||||
if action == "inserted":
|
||||
inserted += 1
|
||||
else:
|
||||
updated += 1
|
||||
time.sleep(0.15)
|
||||
time.sleep(0.5)
|
||||
|
||||
return {"inserted": inserted, "updated": updated, "queries": len(QUERIES), "search_points": len(search_points)}
|
||||
|
||||
|
||||
def sync_places_countries(countries: list[str]) -> dict[str, Any]:
|
||||
if not _api_key():
|
||||
return {"skipped": True, "reason": "GOOGLE_MAPS_API_KEY not set"}
|
||||
out: dict[str, Any] = {}
|
||||
for c in countries:
|
||||
out[c] = sync_places_country(c)
|
||||
time.sleep(1)
|
||||
return out
|
||||
Reference in New Issue
Block a user