feat(browser): auto-commit con 178 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:22:23 +02:00
parent 7d100e7f3e
commit 763e06c127
178 changed files with 19917 additions and 317 deletions
@@ -0,0 +1,108 @@
---
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).