search a ads
This commit is contained in:
+22
-18
@@ -10,8 +10,9 @@ from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
|
||||
from ..clients.ga_client import GoogleAnalyticsClient
|
||||
from ..credentials import GaCredentials, get_ga_credentials
|
||||
from .. import config
|
||||
from ..clients.google import GoogleApiClient
|
||||
from ..credentials import GoogleCredentials, get_ga_credentials
|
||||
|
||||
router = APIRouter(prefix="/ga/admin", tags=["google-analytics: admin"])
|
||||
|
||||
@@ -21,15 +22,18 @@ def _property(property_id: str) -> str:
|
||||
return pid if pid.startswith("properties/") else f"properties/{pid}"
|
||||
|
||||
|
||||
def _client(creds: GoogleCredentials) -> GoogleApiClient:
|
||||
return GoogleApiClient(creds, service_name="Google Analytics")
|
||||
|
||||
|
||||
@router.get("/accounts", summary="List accessible GA4 accounts")
|
||||
async def list_accounts(
|
||||
page_size: int | None = Query(None, ge=1, le=200, alias="pageSize"),
|
||||
page_token: str | None = Query(None, alias="pageToken"),
|
||||
creds: GaCredentials = Depends(get_ga_credentials),
|
||||
creds: GoogleCredentials = Depends(get_ga_credentials),
|
||||
) -> Any:
|
||||
params = _paging(page_size, page_token)
|
||||
client = GoogleAnalyticsClient(creds)
|
||||
return await client.admin_get("/accounts", params=params)
|
||||
return await _client(creds).get(f"{config.GA_ADMIN_BASE_URL}/accounts", params)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -39,11 +43,12 @@ async def list_accounts(
|
||||
async def list_account_summaries(
|
||||
page_size: int | None = Query(None, ge=1, le=200, alias="pageSize"),
|
||||
page_token: str | None = Query(None, alias="pageToken"),
|
||||
creds: GaCredentials = Depends(get_ga_credentials),
|
||||
creds: GoogleCredentials = Depends(get_ga_credentials),
|
||||
) -> Any:
|
||||
params = _paging(page_size, page_token)
|
||||
client = GoogleAnalyticsClient(creds)
|
||||
return await client.admin_get("/accountSummaries", params=params)
|
||||
return await _client(creds).get(
|
||||
f"{config.GA_ADMIN_BASE_URL}/accountSummaries", params
|
||||
)
|
||||
|
||||
|
||||
@router.get("/properties", summary="List properties under an account")
|
||||
@@ -56,14 +61,13 @@ async def list_properties(
|
||||
page_size: int | None = Query(None, ge=1, le=200, alias="pageSize"),
|
||||
page_token: str | None = Query(None, alias="pageToken"),
|
||||
show_deleted: bool | None = Query(None, alias="showDeleted"),
|
||||
creds: GaCredentials = Depends(get_ga_credentials),
|
||||
creds: GoogleCredentials = Depends(get_ga_credentials),
|
||||
) -> Any:
|
||||
params: dict[str, Any] = {"filter": f"parent:accounts/{account_id.strip()}"}
|
||||
params.update(_paging(page_size, page_token))
|
||||
if show_deleted is not None:
|
||||
params["showDeleted"] = show_deleted
|
||||
client = GoogleAnalyticsClient(creds)
|
||||
return await client.admin_get("/properties", params=params)
|
||||
return await _client(creds).get(f"{config.GA_ADMIN_BASE_URL}/properties", params)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -72,10 +76,11 @@ async def list_properties(
|
||||
)
|
||||
async def get_property(
|
||||
property_id: str = Path(..., description="GA4 property id"),
|
||||
creds: GaCredentials = Depends(get_ga_credentials),
|
||||
creds: GoogleCredentials = Depends(get_ga_credentials),
|
||||
) -> Any:
|
||||
client = GoogleAnalyticsClient(creds)
|
||||
return await client.admin_get(f"/{_property(property_id)}")
|
||||
return await _client(creds).get(
|
||||
f"{config.GA_ADMIN_BASE_URL}/{_property(property_id)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -86,12 +91,11 @@ async def list_data_streams(
|
||||
property_id: str = Path(..., description="GA4 property id"),
|
||||
page_size: int | None = Query(None, ge=1, le=200, alias="pageSize"),
|
||||
page_token: str | None = Query(None, alias="pageToken"),
|
||||
creds: GaCredentials = Depends(get_ga_credentials),
|
||||
creds: GoogleCredentials = Depends(get_ga_credentials),
|
||||
) -> Any:
|
||||
params = _paging(page_size, page_token)
|
||||
client = GoogleAnalyticsClient(creds)
|
||||
return await client.admin_get(
|
||||
f"/{_property(property_id)}/dataStreams", params=params
|
||||
return await _client(creds).get(
|
||||
f"{config.GA_ADMIN_BASE_URL}/{_property(property_id)}/dataStreams", params
|
||||
)
|
||||
|
||||
|
||||
|
||||
+26
-32
@@ -13,8 +13,9 @@ from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path
|
||||
|
||||
from ..clients.ga_client import GoogleAnalyticsClient
|
||||
from ..credentials import GaCredentials, get_ga_credentials
|
||||
from .. import config
|
||||
from ..clients.google import GoogleApiClient
|
||||
from ..credentials import GoogleCredentials, get_ga_credentials
|
||||
|
||||
router = APIRouter(prefix="/ga/data", tags=["google-analytics: data"])
|
||||
|
||||
@@ -32,6 +33,14 @@ def _property(property_id: str) -> str:
|
||||
return pid if pid.startswith("properties/") else f"properties/{pid}"
|
||||
|
||||
|
||||
def _client(creds: GoogleCredentials) -> GoogleApiClient:
|
||||
return GoogleApiClient(creds, service_name="Google Analytics")
|
||||
|
||||
|
||||
def _url(property_id: str, suffix: str) -> str:
|
||||
return f"{config.GA_DATA_BASE_URL}/{_property(property_id)}{suffix}"
|
||||
|
||||
|
||||
@router.post(
|
||||
"/properties/{property_id}/runReport",
|
||||
summary="Run a GA4 report",
|
||||
@@ -39,10 +48,9 @@ def _property(property_id: str) -> str:
|
||||
async def run_report(
|
||||
property_id: str = Path(..., description="GA4 property id, e.g. 123456789"),
|
||||
body: dict[str, Any] = Body(..., examples=[_RUN_REPORT_EXAMPLE]),
|
||||
creds: GaCredentials = Depends(get_ga_credentials),
|
||||
creds: GoogleCredentials = Depends(get_ga_credentials),
|
||||
) -> Any:
|
||||
client = GoogleAnalyticsClient(creds)
|
||||
return await client.data_post(f"/{_property(property_id)}:runReport", body)
|
||||
return await _client(creds).post(_url(property_id, ":runReport"), body)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -52,12 +60,9 @@ async def run_report(
|
||||
async def run_pivot_report(
|
||||
property_id: str = Path(..., description="GA4 property id"),
|
||||
body: dict[str, Any] = Body(...),
|
||||
creds: GaCredentials = Depends(get_ga_credentials),
|
||||
creds: GoogleCredentials = Depends(get_ga_credentials),
|
||||
) -> Any:
|
||||
client = GoogleAnalyticsClient(creds)
|
||||
return await client.data_post(
|
||||
f"/{_property(property_id)}:runPivotReport", body
|
||||
)
|
||||
return await _client(creds).post(_url(property_id, ":runPivotReport"), body)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -67,12 +72,9 @@ async def run_pivot_report(
|
||||
async def batch_run_reports(
|
||||
property_id: str = Path(..., description="GA4 property id"),
|
||||
body: dict[str, Any] = Body(...),
|
||||
creds: GaCredentials = Depends(get_ga_credentials),
|
||||
creds: GoogleCredentials = Depends(get_ga_credentials),
|
||||
) -> Any:
|
||||
client = GoogleAnalyticsClient(creds)
|
||||
return await client.data_post(
|
||||
f"/{_property(property_id)}:batchRunReports", body
|
||||
)
|
||||
return await _client(creds).post(_url(property_id, ":batchRunReports"), body)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -82,11 +84,10 @@ async def batch_run_reports(
|
||||
async def batch_run_pivot_reports(
|
||||
property_id: str = Path(..., description="GA4 property id"),
|
||||
body: dict[str, Any] = Body(...),
|
||||
creds: GaCredentials = Depends(get_ga_credentials),
|
||||
creds: GoogleCredentials = Depends(get_ga_credentials),
|
||||
) -> Any:
|
||||
client = GoogleAnalyticsClient(creds)
|
||||
return await client.data_post(
|
||||
f"/{_property(property_id)}:batchRunPivotReports", body
|
||||
return await _client(creds).post(
|
||||
_url(property_id, ":batchRunPivotReports"), body
|
||||
)
|
||||
|
||||
|
||||
@@ -97,12 +98,9 @@ async def batch_run_pivot_reports(
|
||||
async def run_realtime_report(
|
||||
property_id: str = Path(..., description="GA4 property id"),
|
||||
body: dict[str, Any] = Body(...),
|
||||
creds: GaCredentials = Depends(get_ga_credentials),
|
||||
creds: GoogleCredentials = Depends(get_ga_credentials),
|
||||
) -> Any:
|
||||
client = GoogleAnalyticsClient(creds)
|
||||
return await client.data_post(
|
||||
f"/{_property(property_id)}:runRealtimeReport", body
|
||||
)
|
||||
return await _client(creds).post(_url(property_id, ":runRealtimeReport"), body)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -112,12 +110,9 @@ async def run_realtime_report(
|
||||
async def check_compatibility(
|
||||
property_id: str = Path(..., description="GA4 property id"),
|
||||
body: dict[str, Any] = Body(...),
|
||||
creds: GaCredentials = Depends(get_ga_credentials),
|
||||
creds: GoogleCredentials = Depends(get_ga_credentials),
|
||||
) -> Any:
|
||||
client = GoogleAnalyticsClient(creds)
|
||||
return await client.data_post(
|
||||
f"/{_property(property_id)}:checkCompatibility", body
|
||||
)
|
||||
return await _client(creds).post(_url(property_id, ":checkCompatibility"), body)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -126,7 +121,6 @@ async def check_compatibility(
|
||||
)
|
||||
async def get_metadata(
|
||||
property_id: str = Path(..., description="GA4 property id"),
|
||||
creds: GaCredentials = Depends(get_ga_credentials),
|
||||
creds: GoogleCredentials = Depends(get_ga_credentials),
|
||||
) -> Any:
|
||||
client = GoogleAnalyticsClient(creds)
|
||||
return await client.data_get(f"/{_property(property_id)}/metadata")
|
||||
return await _client(creds).get(_url(property_id, "/metadata"))
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Google Search Console - read API.
|
||||
|
||||
Search Analytics, Sites and Sitemaps live under the Webmasters v3 API; URL
|
||||
Inspection lives under searchconsole.googleapis.com/v1. Request/response bodies
|
||||
are forwarded as-is.
|
||||
|
||||
The site URL (e.g. ``https://example.com/`` or ``sc-domain:example.com``) is
|
||||
passed as a query parameter and URL-encoded into the upstream path - this keeps
|
||||
our routes clean and avoids ambiguity with the slashes/colons it contains.
|
||||
|
||||
Credentials: X-GSC-Access-Token (preferred) or X-GSC-Credentials.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Query
|
||||
|
||||
from .. import config
|
||||
from ..clients.google import GoogleApiClient
|
||||
from ..credentials import GoogleCredentials, get_gsc_credentials
|
||||
|
||||
router = APIRouter(prefix="/gsc", tags=["google-search-console"])
|
||||
|
||||
_QUERY_EXAMPLE = {
|
||||
"startDate": "2026-05-01",
|
||||
"endDate": "2026-05-31",
|
||||
"dimensions": ["query", "page"],
|
||||
"rowLimit": 100,
|
||||
}
|
||||
|
||||
_SITE_URL_DESC = (
|
||||
"Property in Search Console: a URL-prefix property (e.g. "
|
||||
"https://example.com/) or a domain property (e.g. sc-domain:example.com)."
|
||||
)
|
||||
|
||||
|
||||
def _client(creds: GoogleCredentials) -> GoogleApiClient:
|
||||
return GoogleApiClient(creds, service_name="Search Console")
|
||||
|
||||
|
||||
def _site(site_url: str) -> str:
|
||||
# The siteUrl is a single path segment and must be fully URL-encoded.
|
||||
return quote(site_url.strip(), safe="")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/searchAnalytics/query",
|
||||
summary="Query Search Console search traffic (clicks, impressions, CTR, position)",
|
||||
)
|
||||
async def search_analytics_query(
|
||||
site_url: str = Query(..., alias="siteUrl", description=_SITE_URL_DESC),
|
||||
body: dict[str, Any] = Body(..., examples=[_QUERY_EXAMPLE]),
|
||||
creds: GoogleCredentials = Depends(get_gsc_credentials),
|
||||
) -> Any:
|
||||
url = f"{config.GSC_DATA_BASE_URL}/sites/{_site(site_url)}/searchAnalytics/query"
|
||||
return await _client(creds).post(url, body)
|
||||
|
||||
|
||||
@router.get("/sites", summary="List sites in the account")
|
||||
async def list_sites(
|
||||
creds: GoogleCredentials = Depends(get_gsc_credentials),
|
||||
) -> Any:
|
||||
return await _client(creds).get(f"{config.GSC_DATA_BASE_URL}/sites")
|
||||
|
||||
|
||||
@router.get("/site", summary="Get a single site's info and permission level")
|
||||
async def get_site(
|
||||
site_url: str = Query(..., alias="siteUrl", description=_SITE_URL_DESC),
|
||||
creds: GoogleCredentials = Depends(get_gsc_credentials),
|
||||
) -> Any:
|
||||
return await _client(creds).get(
|
||||
f"{config.GSC_DATA_BASE_URL}/sites/{_site(site_url)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/sitemaps", summary="List sitemaps submitted for a site")
|
||||
async def list_sitemaps(
|
||||
site_url: str = Query(..., alias="siteUrl", description=_SITE_URL_DESC),
|
||||
creds: GoogleCredentials = Depends(get_gsc_credentials),
|
||||
) -> Any:
|
||||
return await _client(creds).get(
|
||||
f"{config.GSC_DATA_BASE_URL}/sites/{_site(site_url)}/sitemaps"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/sitemap", summary="Get information about a specific sitemap")
|
||||
async def get_sitemap(
|
||||
site_url: str = Query(..., alias="siteUrl", description=_SITE_URL_DESC),
|
||||
feedpath: str = Query(
|
||||
..., description="Full URL of the sitemap, e.g. https://example.com/sitemap.xml"
|
||||
),
|
||||
creds: GoogleCredentials = Depends(get_gsc_credentials),
|
||||
) -> Any:
|
||||
url = (
|
||||
f"{config.GSC_DATA_BASE_URL}/sites/{_site(site_url)}"
|
||||
f"/sitemaps/{quote(feedpath.strip(), safe='')}"
|
||||
)
|
||||
return await _client(creds).get(url)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/urlInspection",
|
||||
summary="Inspect the Google index status of a URL",
|
||||
)
|
||||
async def inspect_url(
|
||||
body: dict[str, Any] = Body(
|
||||
...,
|
||||
examples=[
|
||||
{
|
||||
"inspectionUrl": "https://example.com/some-page",
|
||||
"siteUrl": "https://example.com/",
|
||||
"languageCode": "cs",
|
||||
}
|
||||
],
|
||||
description="Requires inspectionUrl and siteUrl; languageCode is optional.",
|
||||
),
|
||||
creds: GoogleCredentials = Depends(get_gsc_credentials),
|
||||
) -> Any:
|
||||
url = f"{config.GSC_INSPECT_BASE_URL}/urlInspection/index:inspect"
|
||||
return await _client(creds).post(url, body)
|
||||
Reference in New Issue
Block a user