"""Google Ads - reporting via GAQL. GoogleAdsService search / searchStream accept a GAQL query and stream rows back; this covers virtually all Google Ads reporting. Request/response bodies are forwarded as-is. Auth differs from the other Google services: besides the OAuth Bearer token it needs a **developer token** (``X-GAds-Developer-Token`` -> ``developer-token``) and, for manager (MCC) access, an optional ``X-GAds-Login-Customer-Id`` (-> ``login-customer-id``). The API version is configurable via ``GOOGLE_ADS_API_VERSION`` because Google deprecates versions yearly. Credentials: see ``app.credentials.get_google_ads_credentials``. """ from __future__ import annotations from typing import Any from fastapi import APIRouter, Body, Depends, Path from .. import config from ..clients.google import GoogleApiClient from ..credentials import GoogleAdsCredentials, get_google_ads_credentials router = APIRouter(prefix="/googleads", tags=["google-ads"]) _SEARCH_EXAMPLE = { "query": ( "SELECT campaign.id, campaign.name, metrics.impressions, metrics.clicks, " "metrics.cost_micros FROM campaign WHERE segments.date DURING LAST_7_DAYS" ) } def _client(creds: GoogleAdsCredentials) -> GoogleApiClient: headers = {"developer-token": creds.developer_token} if creds.login_customer_id: headers["login-customer-id"] = creds.login_customer_id return GoogleApiClient( creds.google, extra_headers=headers, service_name="Google Ads" ) def _customer(customer_id: str) -> str: # Customer ids are digits only (callers may include dashes for readability). return customer_id.strip().replace("-", "") def _base(customer_id: str) -> str: return ( f"{config.GOOGLE_ADS_BASE_URL}/{config.GOOGLE_ADS_API_VERSION}" f"/customers/{_customer(customer_id)}/googleAds" ) @router.post( "/customers/{customer_id}/search", summary="Run a GAQL query (paginated)", ) async def search( customer_id: str = Path(..., description="Google Ads customer id (digits)"), body: dict[str, Any] = Body(..., examples=[_SEARCH_EXAMPLE]), creds: GoogleAdsCredentials = Depends(get_google_ads_credentials), ) -> Any: return await _client(creds).post(f"{_base(customer_id)}:search", body) @router.post( "/customers/{customer_id}/searchStream", summary="Run a GAQL query (streamed, whole result set in one response)", ) async def search_stream( customer_id: str = Path(..., description="Google Ads customer id (digits)"), body: dict[str, Any] = Body(..., examples=[_SEARCH_EXAMPLE]), creds: GoogleAdsCredentials = Depends(get_google_ads_credentials), ) -> Any: return await _client(creds).post(f"{_base(customer_id)}:searchStream", body) @router.get( "/customers:listAccessibleCustomers", summary="List customer ids the credentials can access", ) async def list_accessible_customers( creds: GoogleAdsCredentials = Depends(get_google_ads_credentials), ) -> Any: url = ( f"{config.GOOGLE_ADS_BASE_URL}/{config.GOOGLE_ADS_API_VERSION}" "/customers:listAccessibleCustomers" ) return await _client(creds).get(url)