""" Google Business Profile Performance API Client =============================================== Uses OAuth 2.0 to fetch visibility and interaction metrics from GBP. Provides daily metrics time series and monthly search keyword impressions. API docs: https://developers.google.com/my-business/reference/performance/rest """ import logging from datetime import datetime, timedelta from typing import Dict, List, Optional import requests logger = logging.getLogger(__name__) # Metrics to request from getDailyMetricsTimeSeries DAILY_METRICS = [ 'BUSINESS_IMPRESSIONS_DESKTOP_MAPS', 'BUSINESS_IMPRESSIONS_MOBILE_MAPS', 'BUSINESS_IMPRESSIONS_DESKTOP_SEARCH', 'BUSINESS_IMPRESSIONS_MOBILE_SEARCH', 'CALL_CLICKS', 'WEBSITE_CLICKS', 'BUSINESS_DIRECTION_REQUESTS', 'BUSINESS_CONVERSATIONS', 'BUSINESS_BOOKINGS', ] class GBPPerformanceService: """Google Business Profile Performance API client.""" BASE_URL = "https://businessprofileperformance.googleapis.com/v1" def __init__(self, access_token: str): self.session = requests.Session() self.session.headers.update({ 'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json', }) self.session.timeout = 20 def get_daily_metrics(self, location_name: str, days: int = 30) -> Dict: """Get aggregated daily metrics for a location. Args: location_name: Full location resource name (e.g., 'locations/123456') days: Number of days to aggregate (default 30) Returns: Dict with aggregated metrics: { 'maps_impressions': int, 'search_impressions': int, 'call_clicks': int, 'website_clicks': int, 'direction_requests': int, 'conversations': int, 'period_days': int, } """ result = { 'maps_impressions': 0, 'search_impressions': 0, 'call_clicks': 0, 'website_clicks': 0, 'direction_requests': 0, 'conversations': 0, 'period_days': days, } end_date = datetime.now() - timedelta(days=1) # Yesterday (today may be incomplete) start_date = end_date - timedelta(days=days) # Normalize location_name format if not location_name.startswith('locations/'): location_name = f'locations/{location_name}' for metric in DAILY_METRICS: try: resp = self.session.get( f"{self.BASE_URL}/{location_name}:getDailyMetricsTimeSeries", params={ 'dailyMetric': metric, 'dailyRange.startDate.year': start_date.year, 'dailyRange.startDate.month': start_date.month, 'dailyRange.startDate.day': start_date.day, 'dailyRange.endDate.year': end_date.year, 'dailyRange.endDate.month': end_date.month, 'dailyRange.endDate.day': end_date.day, } ) if resp.status_code != 200: logger.debug(f"Performance API metric {metric} returned {resp.status_code}") continue data = resp.json() time_series = data.get('timeSeries', {}) daily_values = time_series.get('datedValues', []) total = sum( dv.get('value', 0) for dv in daily_values if dv.get('value') is not None ) # Map metric to result key if 'MAPS' in metric: result['maps_impressions'] += total elif 'SEARCH' in metric: result['search_impressions'] += total elif metric == 'CALL_CLICKS': result['call_clicks'] = total elif metric == 'WEBSITE_CLICKS': result['website_clicks'] = total elif metric == 'BUSINESS_DIRECTION_REQUESTS': result['direction_requests'] = total elif metric == 'BUSINESS_CONVERSATIONS': result['conversations'] = total except Exception as e: logger.debug(f"Performance API metric {metric} failed: {e}") continue logger.info( f"GBP Performance for {location_name}: " f"maps={result['maps_impressions']}, search={result['search_impressions']}, " f"calls={result['call_clicks']}, web={result['website_clicks']}" ) return result def get_search_keywords(self, location_name: str) -> List[Dict]: """Get monthly search keyword impressions. Args: location_name: Full location resource name Returns: List of dicts: [{'keyword': str, 'impressions': int}, ...] """ # Normalize location_name format if not location_name.startswith('locations/'): location_name = f'locations/{location_name}' try: resp = self.session.get( f"{self.BASE_URL}/{location_name}/searchkeywords/impressions/monthly", params={'pageSize': 20} ) if resp.status_code != 200: logger.debug(f"Search keywords API returned {resp.status_code}") return [] data = resp.json() keywords = [] for item in data.get('searchKeywordsCounts', []): keyword = item.get('searchKeyword', '') # Get the most recent month's data monthly = item.get('insightsValue', {}) impressions = monthly.get('value', 0) if keyword: keywords.append({ 'keyword': keyword, 'impressions': impressions, }) # Sort by impressions descending keywords.sort(key=lambda x: x['impressions'], reverse=True) logger.info(f"GBP search keywords for {location_name}: {len(keywords)} keywords") return keywords[:20] except Exception as e: logger.warning(f"GBP search keywords failed for {location_name}: {e}") return [] def get_all_performance_data(self, location_name: str, days: int = 30) -> Dict: """Get all performance data (metrics + keywords) in one call. Returns: Dict with all performance data combined. """ metrics = self.get_daily_metrics(location_name, days) keywords = self.get_search_keywords(location_name) metrics['search_keywords'] = keywords return metrics