Files
fn_registry/python/functions/datascience/pull_gsc_search_analytics.py
T
egutierrez 763e06c127 feat(browser): auto-commit con 178 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-20 18:22:23 +02:00

107 lines
4.0 KiB
Python

"""Extractor de Search Analytics de Google Search Console (GSC).
Consulta la Search Analytics API de Google Search Console y devuelve las filas
aplanadas (impresiones, clicks, CTR, posicion) por las dimensiones pedidas.
Es el extractor principal de datos SEO para alimentar un pipeline hacia
DuckDB/Postgres.
"""
from typing import Any
def pull_gsc_search_analytics(
service: object,
site_url: str,
start_date: str,
end_date: str,
dimensions: list = None,
row_limit: int = 25000,
max_total_rows: int = 0,
search_type: str = "web",
) -> list:
"""Extrae datos de Search Analytics de Google Search Console.
Llama a ``service.searchanalytics().query(...).execute()`` paginando los
resultados (la API devuelve como maximo ``row_limit`` filas por request,
con tope duro de 25000) y aplana cada fila a un dict donde el array ``keys``
se mapea posicionalmente a los nombres de ``dimensions``.
Args:
service: objeto service autenticado de la API de Search Console
(el que devuelve ``gsc_auth`` del registry). Se inyecta ya
construido; esta funcion NO lo crea.
site_url: propiedad de Search Console. ``sc-domain:ejemplo.com`` para
propiedad de dominio, o la URL completa ``https://ejemplo.com/``
para propiedad de prefijo.
start_date: fecha inicial inclusiva en formato ``YYYY-MM-DD``.
end_date: fecha final inclusiva en formato ``YYYY-MM-DD``. La API tiene
~2-3 dias de lag; el caller deberia pedir hasta hoy-3.
dimensions: lista de dimensiones a desglosar. Por defecto
``["query", "page"]``. Otras validas: ``date``, ``country``,
``device``, ``searchAppearance``.
row_limit: filas por request (1..25000). Tambien el tamaño de paso de
la paginacion. Por defecto 25000.
max_total_rows: tope total de filas acumuladas. ``0`` = sin tope (trae
todas las paginas disponibles).
search_type: tipo de busqueda. ``"web"`` | ``"image"`` | ``"video"`` |
``"news"`` | ``"discover"`` | ``"googleNews"``. Va en el body como
``"type"``.
Returns:
Lista de dicts aplanados. Cada dict tiene una clave por cada dimension
(con su nombre real, ej. ``query``, ``page``) mas ``clicks``,
``impressions``, ``ctr`` y ``position``. Lista vacia si la API no
devuelve filas.
Raises:
Exception: cualquier error de la API HTTP de Google se propaga
(autenticacion, permisos sobre la propiedad, rate limit, etc.).
"""
dims = list(dimensions) if dimensions else ["query", "page"]
# Clamp del row_limit al rango valido de la API (1..25000).
page_size = max(1, min(int(row_limit), 25000))
results: list = []
start_row = 0
while True:
body: dict[str, Any] = {
"startDate": start_date,
"endDate": end_date,
"dimensions": dims,
"type": search_type,
"rowLimit": page_size,
"startRow": start_row,
}
response = (
service.searchanalytics().query(siteUrl=site_url, body=body).execute()
)
rows = response.get("rows") if isinstance(response, dict) else None
if not rows:
# rows ausente o vacio => no hay mas datos.
break
for row in rows:
keys = row.get("keys", [])
flat: dict[str, Any] = {}
for i, dim in enumerate(dims):
flat[dim] = keys[i] if i < len(keys) else None
flat["clicks"] = row.get("clicks")
flat["impressions"] = row.get("impressions")
flat["ctr"] = row.get("ctr")
flat["position"] = row.get("position")
results.append(flat)
if max_total_rows > 0 and len(results) >= max_total_rows:
return results[:max_total_rows]
# Si la pagina trajo menos filas que el tope, no hay mas paginas.
if len(rows) < page_size:
break
start_row += page_size
return results