---
name: sync_xlsx_to_dq_checks
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def sync_xlsx_to_dq_checks(xlsx_path: str, project: str = 'autingo-159109', dataset: str = 'data_quality', table: str = 'dq_checks', sheet_name: str = 'catalogo', validate_sql: bool = True) -> dict"
description: "Sincroniza un catalogo de data quality checks desde un archivo Excel (.xlsx) hacia una tabla de BigQuery. Lee la hoja con openpyxl localizando columnas por cabecera, valida/normaliza cada fila (tipo fiabilidad|negocio, severity info|warning|critical, threshold int>=0, enabled booleano flexible), auto-deriva check_id unico cuando viene vacio, hace dry-run de cada assert_sql contra BigQuery para cazar SQL invalido, y carga las filas validas con WRITE_TRUNCATE (reemplazo total: el Excel es la fuente de verdad). Devuelve resumen con loaded/skipped/sql_errors/rows."
tags: [bigquery, data-quality, excel, sync]
uses_functions: [bq_auth_py_infra, bq_query_py_infra, bq_load_from_file_py_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["openpyxl", "csv", "tempfile", "re", "os", "datetime"]
tested: true
tests: ["test_fila_valida_minima_se_carga", "test_check_id_se_autoderiva_cuando_vacio", "test_check_id_explicito_se_respeta", "test_check_id_colision_se_sufija", "test_tipo_invalido_se_skipea", "test_tipo_case_insensitive_se_normaliza", "test_pregunta_vacia_se_skipea", "test_assert_sql_vacia_se_skipea", "test_severity_invalida_se_skipea", "test_threshold_negativo_se_skipea", "test_threshold_no_numerico_se_skipea", "test_fila_totalmente_vacia_se_ignora_sin_skip", "test_parse_bool_variantes", "test_parse_threshold_defaults_y_floats_enteros", "test_slug_colapsa_y_recorta", "test_assert_sql_con_comas_y_comillas_se_conserva"]
test_file_path: "python/functions/infra/sync_xlsx_to_dq_checks_test.py"
file_path: "python/functions/infra/sync_xlsx_to_dq_checks.py"
params:
  - name: xlsx_path
    desc: "Ruta absoluta al archivo .xlsx del catalogo de asserts (fila 1 = cabeceras)."
  - name: project
    desc: "Proyecto GCP destino. Default 'autingo-159109'."
  - name: dataset
    desc: "Dataset BigQuery destino. Default 'data_quality'."
  - name: table
    desc: "Tabla BigQuery destino. Default 'dq_checks'."
  - name: sheet_name
    desc: "Nombre de la hoja del libro a leer. Default 'catalogo'."
  - name: validate_sql
    desc: "Si True, hace un dry-run de cada assert_sql contra BigQuery antes de cargarlo; las filas con SQL invalido NO se cargan y van a sql_errors. Default True."
output: 'dict con {"loaded": int filas cargadas, "skipped": int filas descartadas por validacion, "sql_errors": [{"check_id": str, "error": str}] filas con assert_sql invalido, "rows": [dict] filas cargadas con sus 11 columnas normalizadas}.'
---

## Ejemplo

```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from infra.sync_xlsx_to_dq_checks import sync_xlsx_to_dq_checks

# El xlsx es la fuente de verdad del catalogo de checks.
result = sync_xlsx_to_dq_checks(
    xlsx_path="/mnt/c/Users/egutierrez/Documents/dq_catalogo_asserts.xlsx",
    project="autingo-159109",
    dataset="data_quality",
    table="dq_checks",
    sheet_name="catalogo",
    validate_sql=True,
)
print(f"cargadas={result['loaded']} descartadas={result['skipped']}")
for err in result["sql_errors"]:
    print(f"SQL invalido en {err['check_id']}: {err['error']}")
```

O directamente con `fn run` (lee el path por defecto si no se pasa argumento):

```bash
./fn run sync_xlsx_to_dq_checks /mnt/c/Users/egutierrez/Documents/dq_catalogo_asserts.xlsx
```

## Cuando usarla

Cuando edites el Excel del catalogo de asserts (anadir/quitar/modificar checks) y quieras propagar esos cambios a la tabla `dq_checks` de BigQuery, antes de la corrida horaria que ejecuta los asserts. Es el paso de sincronizacion que convierte el Excel humano en la tabla que consume el motor de data quality.

## Gotchas

- Impura: requiere ADC de gcloud configurado (`gcloud auth application-default login`) con acceso de escritura a BigQuery sobre el proyecto/dataset destino.
- `WRITE_TRUNCATE` borra y reemplaza la tabla `dq_checks` ENTERA en cada corrida. El Excel es la unica fuente de verdad: lo que no este en el Excel desaparece de BigQuery. No es un upsert.
- El dry-run (`validate_sql=True`) consume cuota minima pero hace un round-trip a BigQuery por cada fila — con catalogos grandes la corrida tarda proporcionalmente. Pasa `validate_sql=False` para saltarlo si ya validaste el SQL por otra via.
- `assert_sql` con comas, comillas o saltos de linea se escribe al CSV temporal con `csv.QUOTE_ALL`, de modo que el CSV que carga BigQuery siempre es valido. El archivo temporal se borra al terminar (tambien en caso de error).
- Las columnas se localizan por nombre de cabecera (fila 1), no por posicion: puedes reordenar columnas en el Excel sin romper el sync, pero las cabeceras deben llamarse exactamente `check_id, tipo, subcategoria, pregunta, assert_sql, threshold, severity, enabled, owner`.
- `created_at`/`updated_at` se generan en el momento del sync (mismo timestamp UTC para todas las filas de la corrida) — no se leen del Excel.
- Las filas que no pasan validacion (tipo/pregunta/assert_sql/severity/threshold) NO se cargan y se cuentan en `skipped`; las filas totalmente vacias se ignoran sin contarse.
