Files
2026-06-18 12:43:42 +02:00

92 lines
3.1 KiB
Python

"""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)