chore: auto-commit (26 archivos)

- python/functions/bigquery/bq_auth.md
- python/functions/bigquery/bq_load_from_file.md
- python/functions/bigquery/bq_load_from_gcs.md
- python/functions/bigquery/client.py
- python/functions/bigquery/queries.py
- python/functions/datascience/__init__.py
- python/functions/datascience/decode_qr_image.py
- python/functions/datascience/load_bq_table_to_duckdb.md
- python/functions/datascience/load_bq_table_to_duckdb.py
- python/functions/pipelines/profile_bq_table.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 19:00:13 +02:00
parent 2ebc9efeb2
commit 5a4f82cf76
26 changed files with 2573 additions and 94 deletions
@@ -0,0 +1,142 @@
---
id: build_column_dictionary_py_datascience
name: build_column_dictionary
kind: function
lang: py
domain: datascience
version: "1.0.0"
purity: pure
signature: "def build_column_dictionary(db_profile: dict) -> dict"
description: "Construye el diccionario de columnas BUSCABLE de una base entera a partir del DatabaseProfile que emite profile_database (grupo eda). Aplana db_profile['table_profiles'] (lista de TableProfile con table y columns) en una entrada por columna con tabla, tipo inferido, tipo semantico, marca de PII (RGPD/LOPDGDD), %null, cardinalidad y valores top. Responde a nivel de base 'donde esta el customer_id / telefono / IBAN'. Emite tambien pii_columns y un markdown grep-able ordenado por columna, precedido de las columnas compartidas por nombre entre tablas (candidatas a join key cross-tabla). Funcion pura, dict-no-throw, no muta el input."
tags: [eda, relations]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
example: |
from datascience import build_column_dictionary
db_profile = {"table_profiles": [
{"table": "clientes", "columns": [
{"name": "email", "inferred_type": "text", "semantic_type": "email",
"null_pct": 0.05, "distinct_count": 990}]}]}
res = build_column_dictionary(db_profile)
# res["pii_columns"] -> [{"table": "clientes", "column": "email", "is_pii": True, ...}]
tested: true
tests:
- "test_golden_flattens_two_tables"
- "test_pii_flagged_from_semantic_type"
- "test_empty_semantic_type_maps_to_none_and_not_pii"
- "test_shared_column_names_detected_as_join_keys"
- "test_top_values_from_categorical_block"
- "test_empty_profile_returns_empty_ok"
- "test_malformed_input_returns_empty_ok"
- "test_missing_keys_read_defensively"
- "test_does_not_mutate_input"
test_file_path: "python/functions/datascience/build_column_dictionary_test.py"
file_path: "python/functions/datascience/build_column_dictionary.py"
params:
- name: db_profile
desc: >
DatabaseProfile del grupo eda tal como lo devuelve profile_database en su
clave db_profile (el dict con table_profiles). table_profiles es una lista
de TableProfile; de cada uno se leen table (nombre) y columns (lista de
ColumnProfile). De cada ColumnProfile se leen defensivamente con .get(...):
name, inferred_type (numeric|categorical|datetime|text|boolean),
semantic_type ("" que se normaliza a None; los que emite infer_semantic_type:
email, iban, credit_card, phone_intl, postal_code_es, ...), null_pct
(fraccion 0-1), distinct_count (cardinalidad, expuesta como n_distinct) y el
bloque categorical.top (para top_values). Una entrada vacia, None o
malformada produce el resultado vacio en estado ok (nunca lanza).
output: >
dict dict-no-throw con status ("ok" siempre), n_tables (int, tablas con columnas
procesadas), n_columns (int total de columnas), entries (list[dict] una por
columna con table, column, inferred_type, semantic_type|None, is_pii (bool),
null_pct (float 0-1|None), n_distinct (int|None), top_values (list[str]|None)),
pii_columns (subconjunto de entries con is_pii=True: dato personal segun
[POL-MMNSEG-001-1.0]) y markdown (str, tabla grep-able ordenada por nombre de
columna precedida de las columnas compartidas por nombre entre tablas). Entrada
vacia o malformada -> n_tables/n_columns 0, listas vacias, markdown "".
---
## Ejemplo
```python
from datascience import build_column_dictionary
# db_profile minimo de juguete (forma de la clave db_profile de profile_database).
db_profile = {
"table_profiles": [
{
"table": "clientes",
"columns": [
{"name": "customer_id", "inferred_type": "numeric",
"semantic_type": "", "null_pct": 0.0, "distinct_count": 1000},
{"name": "email", "inferred_type": "text",
"semantic_type": "email", "null_pct": 0.05, "distinct_count": 990},
{"name": "ciudad", "inferred_type": "categorical",
"semantic_type": "", "null_pct": 0.0, "distinct_count": 3,
"categorical": {"top": [
{"value": "Madrid", "count": 5, "pct": 0.5},
{"value": "Bilbao", "count": 3, "pct": 0.3}]}},
],
},
{
"table": "pedidos",
"columns": [
{"name": "customer_id", "inferred_type": "numeric",
"semantic_type": "", "null_pct": 0.0, "distinct_count": 800},
{"name": "iban", "inferred_type": "text",
"semantic_type": "iban", "null_pct": 0.1, "distinct_count": 795},
],
},
]
}
res = build_column_dictionary(db_profile)
print(res["n_tables"], res["n_columns"]) # 2 5
print([(e["table"], e["column"]) for e in res["pii_columns"]])
# [('clientes', 'email'), ('pedidos', 'iban')]
print(res["markdown"]) # tabla grep-able + seccion de join keys (customer_id)
```
Uso real componiendo con `profile_database` (perfila la base y construye el diccionario):
```python
from pipelines.profile_database import profile_database
from datascience import build_column_dictionary
r = profile_database("mi_base.duckdb", write_report=False)
if r["status"] == "ok":
dicc = build_column_dictionary(r["db_profile"])
# grep sobre dicc["markdown"] para localizar donde vive cada dato,
# dicc["pii_columns"] para el inventario RGPD de la base.
```
## Cuando usarla
Usala cuando necesites un indice tabla.columna de una base ENTERA: para localizar
por busqueda "donde esta el customer_id / telefono / IBAN" antes de escribir un
join, para descubrir claves de join cross-tabla (columnas con el mismo nombre en
varias tablas) o para levantar un inventario de columnas con datos personales
(RGPD/LOPDGDD) sobre el que auditar. Es el paso natural despues de
`profile_database`: toma su `db_profile` y lo convierte en diccionario buscable.
## Gotchas
- El criterio de PII se basa SOLO en el `semantic_type` que hoy emite el grupo
`eda` (`infer_semantic_type`): se marcan email, phone_intl, iban, credit_card y
postal_code_es. El catalogo de regex NO detecta hoy nombre de persona ni DNI/NIE,
asi que esas columnas caen como texto/categorico y NO se marcan automaticamente.
Politica [POL-MMNSEG-001-1.0]: ante cualquier duda sobre si una columna contiene
datos personales, tratala como PII y avisa antes de exponerla; `pii_columns` es
una ayuda, no un inventario RGPD exhaustivo.
- `n_distinct` se lee de la clave `distinct_count` del ColumnProfile (no de
`categorical.n_distinct`); en tablas grandes puede venir de `approx_unique`
(HyperLogLog) capado a n_rows, no exacto.
- `top_values` solo se rellena si la columna trae bloque `categorical` (lo pone
`profile_table` para columnas categorical/text); las numericas/datetime lo
dejan en None.
- Funcion pura: no toca disco ni muta el input. NO perfila la base — eso lo hace
`profile_database`; aqui solo se APLANA su salida.