--- name: ingest_gsc_search_analytics kind: pipeline lang: py domain: pipelines version: "1.0.0" purity: impure signature: "def ingest_gsc_search_analytics(site_url: str = '', duckdb_path: str = '', pg_dsn: str = '', start_date: str = '', end_date: str = '', lookback_days: int = 5, credentials_path: str = '') -> dict" description: "Pipeline de ingesta diaria de Google Search Console (Search Analytics): GSC -> DuckDB -> PostgreSQL. Autentica con una service account (gsc_auth), extrae las filas de Search Analytics por las dimensiones date/query/page (pull_gsc_search_analytics), crea la tabla DuckDB si no existe con una restriccion UNIQUE (duckdb_execute), transforma cada fila renombrando 'date'->'data_date' y rellenando defaults estables (country='', device='', search_type='web') para las dimensiones no pedidas, hace upsert idempotente en DuckDB (duckdb_upsert) y espeja la tabla completa a PostgreSQL en modo replace para que Metabase la lea (duckdb_to_postgres). DuckDB es la verdad acumulada (historico append idempotente); PostgreSQL es un espejo regenerado por completo cada corrida. Resuelve defaults de site_url/pg_dsn/duckdb_path desde env (GSC_SITE_URL, SEO_DSN, SEO_DUCKDB con fallback ~/.fn_seo/seo.duckdb). Resuelve fechas teniendo en cuenta el lag de ~3 dias de la API: end=hoy-3, start=hoy-(3+lookback_days), re-pulleando los ultimos dias para que el upsert corrija lo que GSC ajusta a posteriori. Devuelve un dict sin lanzar: {status:'ok', site_url, start_date, end_date, rows_pulled, duckdb, postgres} en exito, {status:'error', error} en fallo." tags: [seo, gsc, search-console, pipelines, duckdb] uses_functions: - gsc_auth_py_infra - pull_gsc_search_analytics_py_datascience - duckdb_execute_py_infra - duckdb_upsert_py_infra - duckdb_to_postgres_py_pipelines uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [os, datetime] params: - name: site_url desc: "propiedad de Search Console: 'sc-domain:ejemplo.com' (propiedad de dominio) o la URL de prefijo 'https://ejemplo.com/'. Si esta vacio se lee de la env var GSC_SITE_URL. Obligatorio: ValueError si falta." - name: duckdb_path desc: "ruta al archivo DuckDB de la fuente de verdad acumulada. Si esta vacio se lee de la env var SEO_DUCKDB y, en su defecto, ~/.fn_seo/seo.duckdb. El directorio padre se crea (os.makedirs exist_ok=True)." - name: pg_dsn desc: "cadena de conexion PostgreSQL del espejo BI, p.ej. 'postgresql://user:pass@host:5432/db'. Si esta vacio se lee de la env var SEO_DSN. Obligatorio: ValueError si falta." - name: start_date desc: "fecha inicial inclusiva 'YYYY-MM-DD'. Si esta vacia se calcula como hoy-(3+lookback_days)." - name: end_date desc: "fecha final inclusiva 'YYYY-MM-DD'. Si esta vacia se calcula como hoy-3 (lag de la API de GSC)." - name: lookback_days desc: "numero de dias extra hacia atras que se re-pullean para que el upsert idempotente corrija los datos que GSC ajusta a posteriori (hasta ~3 dias). Default 5." - name: credentials_path desc: "ruta al JSON de la service account. Se pasa tal cual a gsc_auth, que ya hace su propio fallback a la env var GSC_SA_JSON." output: "dict. En exito: {status:'ok', site_url:str, start_date:str, end_date:str, rows_pulled:int, duckdb:dict (resultado de duckdb_upsert), postgres:dict (resultado de duckdb_to_postgres)}. En error (sin lanzar): {status:'error', error:str}." tested: true tests: - "test_renombra_date_a_data_date_y_persiste_en_duckdb" - "test_resolucion_fechas_por_defecto" - "test_upsert_idempotente_no_duplica" - "test_falta_site_url_da_value_error" - "test_falta_pg_dsn_da_value_error" test_file_path: "python/functions/pipelines/ingest_gsc_search_analytics_test.py" file_path: "python/functions/pipelines/ingest_gsc_search_analytics.py" --- ## Ejemplo ```bash # Con las 3 env seteadas, una sola corrida hace el snapshot diario completo: export GSC_SITE_URL="sc-domain:ejemplo.com" export SEO_DSN="postgresql://seo:****@127.0.0.1:5432/seo" export GSC_SA_JSON="$HOME/.fn_seo/service_account.json" # (SEO_DUCKDB opcional; por defecto ~/.fn_seo/seo.duckdb) ./fn run ingest_gsc_search_analytics # -> {"status": "ok", "site_url": "sc-domain:ejemplo.com", # "start_date": "2026-06-09", "end_date": "2026-06-17", # "rows_pulled": 1280, "duckdb": {...}, "postgres": {...}} ``` ```python import sys sys.path.insert(0, "python/functions") from pipelines.ingest_gsc_search_analytics import ingest_gsc_search_analytics # Variante explicita: rango de fechas fijo y rutas pasadas como args. res = ingest_gsc_search_analytics( site_url="sc-domain:ejemplo.com", duckdb_path="/home/me/.fn_seo/seo.duckdb", pg_dsn="postgresql://seo:****@127.0.0.1:5432/seo", start_date="2026-06-01", end_date="2026-06-17", credentials_path="/home/me/.fn_seo/service_account.json", ) print(res["rows_pulled"], res["status"]) # 4210 ok ``` ## Cuando usarla Cuando quieras un snapshot diario de Google Search Console acumulado y consultable desde Metabase: cada corrida añade/actualiza los datos del rango en DuckDB y regenera el espejo PostgreSQL. La invoca el DAG `seo-gsc-daily` de dag_engine una vez al dia (no uses cron ni systemd timers: usa dag_engine). Para un re-pull manual puntual de un rango concreto, pásale `start_date`/`end_date` a mano. ## Gotchas - **Lag de ~3 dias**: la API de GSC no consolida datos hasta ~3 dias despues. Por eso `end_date` por defecto es hoy-3 y `start_date` retrocede `lookback_days` extra. Pedir hasta hoy devolveria filas vacias o incompletas. - **Re-pull idempotente**: se re-piden a proposito los ultimos `lookback_days` dias. La restriccion `UNIQUE (site_url, data_date, query, page, country, device, search_type)` + `duckdb_upsert` actualizan esas filas sin duplicarlas, recogiendo las correcciones que GSC aplica a posteriori. El `snapshot_date` se sobrescribe al valor de la ultima corrida. - **DuckDB es la verdad; PostgreSQL es un espejo**: la ingesta acumula histórico solo en DuckDB. El espejo a Postgres usa `mode='replace'` -> hace DROP + CREATE + INSERT de la tabla completa cada vez. NO escribas en la tabla Postgres ni esperes acumular alli: se borra y reescribe en cada corrida. Si quieres histórico, leelo de DuckDB. - **Dimensiones**: este pull pide solo `date`/`query`/`page`. `country` y `device` quedan vacios y `search_type='web'` como defaults estables para que la tupla UNIQUE sea consistente. Si necesitas desglose por pais/dispositivo, es otro pull/tabla. - **Requisitos de entorno**: necesita las 3 env (`GSC_SITE_URL`, `SEO_DSN`, `GSC_SA_JSON`) o sus args equivalentes, y la service account debe estar añadida como usuario con permiso sobre la propiedad en Search Console. Faltar `site_url` o `pg_dsn` devuelve `{status:'error'}` (ValueError capturado, no crash).