chore: auto-commit (8 archivos)
- scratchpad/gen_docs.py - scratchpad/gen_intel.py - scratchpad/gen_verify.py - scratchpad/intel_build.json - scratchpad/intel_lineage.json - scratchpad/lineage_graph.json - scratchpad/trace_intel.py - scratchpad/trace_lineage.py Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,313 @@
|
|||||||
|
"""Genera la carpeta de documentacion de linaje en el Escritorio de Windows.
|
||||||
|
|
||||||
|
A partir del grafo trazado (scratchpad/lineage_graph.json) escribe:
|
||||||
|
00_INDICE.txt resumen + mapa de capas + tabla de todos los objetos
|
||||||
|
01_marts/<vista>.txt una por vista de customer_marts: que es + arbol de linaje + SQL
|
||||||
|
02_intermedio_clientes_intel/*.txt tablas base del pipeline de inteligencia de clientes
|
||||||
|
03_producto/*.txt cadena de catalogo de producto (vistas con SQL + bases)
|
||||||
|
04_fuentes/*.txt tablas fuente (replica Postgres, Navision, imagenes, tasas)
|
||||||
|
|
||||||
|
Todos los .txt se escriben con CRLF para abrirse limpios en Bloc de notas de Windows.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
DEST = "/mnt/c/Users/egutierrez/Desktop/linaje_customer_marts"
|
||||||
|
DATA = json.load(open("scratchpad/lineage_graph.json"))
|
||||||
|
G = DATA["graph"]
|
||||||
|
PROJECT = DATA["project"]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Descripciones ("que es") por objeto. La SQL/DDL incluida en cada archivo es la
|
||||||
|
# fuente de verdad; estas lineas son un resumen para orientar al lector.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
DESC = {
|
||||||
|
# ---- customer_marts (marts finales, grano = persona_id / cliente) ----
|
||||||
|
"customer_marts.customer_profile":
|
||||||
|
"Ficha maestra 360 del cliente: identidad + features agregadas + score CLV + segmento. Vista de perfil que consolida todo lo demas.",
|
||||||
|
"customer_marts.customer_monetary":
|
||||||
|
"Metricas monetarias del cliente (gasto total, ticket medio, recencia/frecuencia/valor). Componente M del RFM.",
|
||||||
|
"customer_marts.customer_channel":
|
||||||
|
"Canal del cliente: canal preferido transaccional, mix aurgi/motortown/web/servicio, canal de entrada (canal8) y fuentes de origen.",
|
||||||
|
"customer_marts.customer_contactability":
|
||||||
|
"Contactabilidad del cliente: disponibilidad de email/telefono y consentimientos, a partir de la dimension persona + features + segmento.",
|
||||||
|
"customer_marts.customer_category_spend":
|
||||||
|
"Gasto del cliente desglosado por categoria de producto, a partir de la tabla de hechos de transaccion.",
|
||||||
|
"customer_marts.customer_brand_affinity":
|
||||||
|
"Afinidad de marca del cliente: que marcas compra y con que peso, cruzando transacciones con el catalogo de producto (Objeto_productos).",
|
||||||
|
"customer_marts.customer_product":
|
||||||
|
"Productos comprados por el cliente (detalle de que ha adquirido) desde la tabla de hechos de transaccion.",
|
||||||
|
"customer_marts.customer_store_spend":
|
||||||
|
"Gasto del cliente por centro/tienda desde la tabla de hechos de transaccion.",
|
||||||
|
"customer_marts.customer_temporal":
|
||||||
|
"Patrones temporales de compra del cliente (estacionalidad, recencia, frecuencia) desde transacciones + features.",
|
||||||
|
"customer_marts.customer_vehicles":
|
||||||
|
"Vehiculos asociados al cliente: dimension vehiculo + features de vehiculo + mapping N:N persona-vehiculo.",
|
||||||
|
"customer_marts.customer_payment_method":
|
||||||
|
"Metodo de pago del cliente reconstruido desde los pedidos TPV (orders/invoice/payment/payment_types).",
|
||||||
|
"customer_marts.customer_promo_usage":
|
||||||
|
"Uso de promociones/descuentos por el cliente (pedidos con descuento) desde transacciones + pedidos TPV + segmento.",
|
||||||
|
"customer_marts.customer_promo_tolerance":
|
||||||
|
"Tolerancia del cliente a promociones: respuesta a campanas + sensibilidad a descuentos en pedidos.",
|
||||||
|
"customer_marts.customer_predictive":
|
||||||
|
"Senales predictivas del cliente: score CLV, proxima mejor accion (recomendaciones) y segmento.",
|
||||||
|
|
||||||
|
# ---- clientes_intel (capa intermedia; tablas base del pipeline de inteligencia de clientes) ----
|
||||||
|
"clientes_intel.dim_persona":
|
||||||
|
"Dimension PERSONA: identidad de cliente consolidada (una fila por persona_id). Nucleo de la doble identidad persona+vehiculo.",
|
||||||
|
"clientes_intel.dim_vehiculo":
|
||||||
|
"Dimension VEHICULO: una fila por vehiculo (matricula/bastidor) con sus atributos.",
|
||||||
|
"clientes_intel.fact_transaccion":
|
||||||
|
"Tabla de HECHOS de transaccion: linea/venta por cliente. Base de casi todos los marts monetarios y de producto.",
|
||||||
|
"clientes_intel.fact_campana_respuesta":
|
||||||
|
"Tabla de HECHOS de respuesta a campanas de marketing (envio/apertura/conversion) por cliente.",
|
||||||
|
"clientes_intel.feat_cliente_persona":
|
||||||
|
"Features agregadas a nivel PERSONA (RFM, mix de canal, indicadores derivados). Alimenta perfil, monetary, channel, temporal, contactability.",
|
||||||
|
"clientes_intel.feat_cliente_vehiculo":
|
||||||
|
"Features agregadas a nivel VEHICULO. Alimenta customer_vehicles.",
|
||||||
|
"clientes_intel.seg_cliente_360":
|
||||||
|
"Segmentacion 360 del cliente (segmentos de negocio / clusters). Alimenta perfil, channel, contactability, predictive, promo_usage.",
|
||||||
|
"clientes_intel.score_clv":
|
||||||
|
"Score de valor de vida del cliente (CLV). Alimenta perfil y predictive.",
|
||||||
|
"clientes_intel.reco_acciones":
|
||||||
|
"Recomendaciones / proxima mejor accion (NBA) por cliente. Alimenta customer_predictive.",
|
||||||
|
"clientes_intel.map_persona_canal8":
|
||||||
|
"Mapeo persona -> canal8 (canal de entrada). Puente para customer_channel.",
|
||||||
|
"clientes_intel.map_persona_fuente":
|
||||||
|
"Mapeo persona -> fuente(s) de origen (de que sistema/canal proviene el cliente). Puente para customer_channel.",
|
||||||
|
"clientes_intel.map_persona_vehiculo":
|
||||||
|
"Mapeo N:N persona <-> vehiculo. Puente para customer_vehicles.",
|
||||||
|
|
||||||
|
# ---- cadena de catalogo de producto ----
|
||||||
|
"anjana_bi_datamart.Objeto_productos":
|
||||||
|
"Vista maestra de PRODUCTO: catalogo Navision + categorias CGQ + imagenes + tasa/margen por material. Se usa para afinidad de marca.",
|
||||||
|
"anjana_bi_datamart.Cruce_16_07_cgq":
|
||||||
|
"Tabla de cruce de categorias CGQ (categoria/subcategoria/tipo) usada por Objeto_productos.",
|
||||||
|
"claude_bi.productos_tasa_mat":
|
||||||
|
"Tabla de tasa/margen por material de producto. La consume Objeto_productos.",
|
||||||
|
"external_datasets.product_object_images":
|
||||||
|
"Imagenes de producto (imagen principal/secundaria). Dataset externo. La consume Objeto_productos.",
|
||||||
|
"stg_anjana_bi.producto":
|
||||||
|
"Staging de producto: cruza item de Navision con equivalencias de matriculas (SAF). Capa de preparacion sobre las tablas de SQL Server.",
|
||||||
|
|
||||||
|
# ---- fuentes base ----
|
||||||
|
"psql_dcpublic.products":
|
||||||
|
"Catalogo de productos. Replica en BigQuery de la BBDD Postgres ANJANA (DCPublic).",
|
||||||
|
"psql_dcpublic.product_categories":
|
||||||
|
"Categorias de producto. Replica Postgres ANJANA (DCPublic).",
|
||||||
|
"psql_dcpublic.product_groups":
|
||||||
|
"Grupos de producto. Replica Postgres ANJANA (DCPublic).",
|
||||||
|
"psql_dcpublic.tpv_orders_order":
|
||||||
|
"Pedidos TPV (cabecera de pedido). Replica Postgres ANJANA (DCPublic).",
|
||||||
|
"psql_dcpublic.tpv_orders_orderitem":
|
||||||
|
"Lineas de pedido TPV. Replica Postgres ANJANA (DCPublic).",
|
||||||
|
"psql_dcpublic.tpv_orders_invoice":
|
||||||
|
"Facturas TPV. Replica Postgres ANJANA (DCPublic).",
|
||||||
|
"psql_dcpublic.tpv_orders_payment":
|
||||||
|
"Pagos de pedidos TPV. Replica Postgres ANJANA (DCPublic).",
|
||||||
|
"psql_dcpublic.tpv_payment_types":
|
||||||
|
"Tipos de pago TPV (catalogo). Replica Postgres ANJANA (DCPublic).",
|
||||||
|
"mssql2022_dbo.item":
|
||||||
|
"Catalogo de articulos de Navision (SQL Server 2022, esquema dbo).",
|
||||||
|
"mssql2022_dbo.equivalencias_matriculas_saf":
|
||||||
|
"Equivalencias de matriculas (SAF) en Navision (SQL Server 2022, esquema dbo).",
|
||||||
|
}
|
||||||
|
|
||||||
|
TYPE_ES = {
|
||||||
|
"VIEW": "VISTA (tiene SQL propio)",
|
||||||
|
"MATERIALIZED VIEW": "VISTA MATERIALIZADA (tiene SQL propio)",
|
||||||
|
"BASE TABLE": "TABLA BASE (datos materializados; sin SQL de definicion, solo esquema)",
|
||||||
|
"EXTERNAL": "TABLA EXTERNA",
|
||||||
|
"UNKNOWN": "DESCONOCIDO",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Carpeta destino por objeto.
|
||||||
|
def folder_of(key: str) -> str:
|
||||||
|
ds = key.split(".", 1)[0]
|
||||||
|
if ds == "customer_marts":
|
||||||
|
return "01_marts"
|
||||||
|
if ds == "clientes_intel":
|
||||||
|
return "02_intermedio_clientes_intel"
|
||||||
|
if ds in ("anjana_bi_datamart", "claude_bi", "external_datasets", "stg_anjana_bi"):
|
||||||
|
return "03_producto"
|
||||||
|
return "04_fuentes"
|
||||||
|
|
||||||
|
def fname_of(key: str) -> str:
|
||||||
|
return key.replace(".", "__") + ".txt"
|
||||||
|
|
||||||
|
def relpath_of(key: str) -> str:
|
||||||
|
return f"{folder_of(key)}/{fname_of(key)}"
|
||||||
|
|
||||||
|
def desc_of(key: str) -> str:
|
||||||
|
return DESC.get(key, "(sin descripcion)")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Arbol de linaje recursivo (para los marts).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def render_tree(key: str, prefix: str | None = None, is_last: bool = True, seen=None) -> list[str]:
|
||||||
|
if seen is None:
|
||||||
|
seen = set()
|
||||||
|
tag = {"VIEW": "[vista]", "MATERIALIZED VIEW": "[vista mat]",
|
||||||
|
"BASE TABLE": "[TABLA BASE/FUENTE]", "EXTERNAL": "[externa]",
|
||||||
|
"UNKNOWN": "[?]"}.get(G.get(key, {"type": "UNKNOWN"})["type"], "")
|
||||||
|
|
||||||
|
if prefix is None: # raiz
|
||||||
|
lines = [f"{key} {tag}"]
|
||||||
|
child_prefix = ""
|
||||||
|
else:
|
||||||
|
connector = "└── " if is_last else "├── "
|
||||||
|
lines = [f"{prefix}{connector}{key} {tag}"]
|
||||||
|
child_prefix = prefix + (" " if is_last else "│ ")
|
||||||
|
|
||||||
|
if key in seen:
|
||||||
|
lines[-1] += " (ya expandido arriba)"
|
||||||
|
return lines
|
||||||
|
seen.add(key)
|
||||||
|
refs = G.get(key, {"refs": []}).get("refs", [])
|
||||||
|
for i, r in enumerate(refs):
|
||||||
|
lines += render_tree(r, child_prefix, i == len(refs) - 1, seen)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Escritura.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def w(path: str, text: str):
|
||||||
|
full = os.path.join(DEST, path)
|
||||||
|
os.makedirs(os.path.dirname(full), exist_ok=True)
|
||||||
|
with open(full, "w", newline="\r\n", encoding="utf-8") as f:
|
||||||
|
f.write(text)
|
||||||
|
|
||||||
|
SEP = "=" * 78 + "\n"
|
||||||
|
|
||||||
|
def object_file(key: str, include_tree: bool) -> str:
|
||||||
|
node = G[key]
|
||||||
|
out = []
|
||||||
|
out.append(SEP)
|
||||||
|
out.append(f"OBJETO : {PROJECT}.{key}\n")
|
||||||
|
out.append(f"TIPO : {TYPE_ES.get(node['type'], node['type'])}\n")
|
||||||
|
out.append(f"DATASET: {key.split('.',1)[0]}\n")
|
||||||
|
out.append(SEP)
|
||||||
|
out.append("\nQUE ES\n------\n")
|
||||||
|
out.append(textwrap.fill(desc_of(key), width=78) + "\n")
|
||||||
|
|
||||||
|
if node.get("refs"):
|
||||||
|
out.append("\nDEPENDE DIRECTAMENTE DE\n-----------------------\n")
|
||||||
|
for r in node["refs"]:
|
||||||
|
out.append(f" - {PROJECT}.{r} -> ver {relpath_of(r)}\n")
|
||||||
|
|
||||||
|
if include_tree:
|
||||||
|
out.append("\nLINAJE COMPLETO (hasta la fuente)\n---------------------------------\n")
|
||||||
|
out.append("\n".join(render_tree(key)) + "\n")
|
||||||
|
|
||||||
|
out.append("\nSQL / DDL\n---------\n")
|
||||||
|
if node["type"] in ("VIEW", "MATERIALIZED VIEW"):
|
||||||
|
out.append("(Definicion de la vista. Este es el SQL que puedes copiar.)\n\n")
|
||||||
|
else:
|
||||||
|
out.append("(Tabla base: no tiene SQL de transformacion. Se incluye el CREATE TABLE\n"
|
||||||
|
" con el esquema de columnas para referencia.)\n\n")
|
||||||
|
out.append(node["ddl"].strip() + "\n")
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
# Marts: incluir arbol de linaje.
|
||||||
|
marts = sorted(k for k in G if k.startswith("customer_marts."))
|
||||||
|
for k in marts:
|
||||||
|
w(f"01_marts/{fname_of(k)}", object_file(k, include_tree=True))
|
||||||
|
|
||||||
|
# Resto de objetos: sin arbol (o arbol solo si es vista con dependencias).
|
||||||
|
for k in sorted(G):
|
||||||
|
if k.startswith("customer_marts."):
|
||||||
|
continue
|
||||||
|
include_tree = G[k]["type"] in ("VIEW", "MATERIALIZED VIEW") and bool(G[k].get("refs"))
|
||||||
|
w(relpath_of(k), object_file(k, include_tree=include_tree))
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# INDICE.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
idx = []
|
||||||
|
idx.append(SEP)
|
||||||
|
idx.append("INDICE - LINAJE DEL DATASET customer_marts\n")
|
||||||
|
idx.append(f"Proyecto BigQuery: {PROJECT}\n")
|
||||||
|
idx.append(SEP)
|
||||||
|
idx.append("""
|
||||||
|
QUE ES ESTA CARPETA
|
||||||
|
-------------------
|
||||||
|
Documenta, para cada tabla/vista del dataset `customer_marts`, de donde salen sus
|
||||||
|
datos: la cadena completa desde el mart final hasta las tablas fuente, con el SQL
|
||||||
|
de cada vista listo para copiar y compartir.
|
||||||
|
|
||||||
|
Cada objeto tiene su propio .txt con:
|
||||||
|
- QUE ES (resumen de una linea; la SQL es la fuente de verdad)
|
||||||
|
- DE QUE DEPENDE (dependencias directas, con la ruta a su archivo)
|
||||||
|
- LINAJE COMPLETO (arbol hasta la fuente) -- solo en los marts y vistas
|
||||||
|
- SQL / DDL (el codigo: definicion de la vista, o el esquema si es tabla base)
|
||||||
|
|
||||||
|
MAPA DE CAPAS
|
||||||
|
-------------
|
||||||
|
customer_marts (VISTAS finales, grano = cliente/persona_id)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
clientes_intel (TABLAS BASE: capa intermedia construida por el pipeline de
|
||||||
|
| inteligencia de clientes -- dim_*, feat_*, seg_*, score_*,
|
||||||
|
| reco_*, fact_*, map_*)
|
||||||
|
v
|
||||||
|
Fuentes:
|
||||||
|
- psql_dcpublic.* Replica en BigQuery de la BBDD Postgres ANJANA (TPV + catalogo)
|
||||||
|
- anjana_bi_datamart / claude_bi / external_datasets / stg_anjana_bi
|
||||||
|
Cadena de catalogo de PRODUCTO (Objeto_productos y sus fuentes)
|
||||||
|
- mssql2022_dbo.* Navision (SQL Server 2022, esquema dbo)
|
||||||
|
|
||||||
|
NOTA: las tablas de `clientes_intel` son TABLAS BASE: no son vistas, sino tablas que
|
||||||
|
un pipeline reconstruye cada dia con sentencias CREATE TABLE AS SELECT (CTAS). Su
|
||||||
|
esquema esta en 02_intermedio_clientes_intel/. El SQL REAL que las construye (y que
|
||||||
|
baja hasta TPV / customers / users / Navision / Salesforce) esta en la carpeta
|
||||||
|
05_construccion_clientes_intel/ -- ver tambien 00b_FUENTES_DE_CLIENTE.txt.
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
idx.append(SEP)
|
||||||
|
idx.append("CARPETAS\n")
|
||||||
|
idx.append(SEP)
|
||||||
|
idx.append("""
|
||||||
|
01_marts/ Las 14 vistas de customer_marts (con arbol de linaje)
|
||||||
|
02_intermedio_clientes_intel/ Las 12 tablas base intermedias (esquema)
|
||||||
|
03_producto/ Cadena de catalogo de producto (vistas + bases)
|
||||||
|
04_fuentes/ Tablas fuente (replica Postgres, Navision, imagenes, tasas)
|
||||||
|
05_construccion_clientes_intel/ El SQL (CTAS) que construye cada tabla de clientes_intel
|
||||||
|
00b_FUENTES_DE_CLIENTE.txt Que consulta lee cada fuente de cliente (TPV/customers/
|
||||||
|
users/Navision/Salesforce)
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
def index_block(title, keys):
|
||||||
|
lines = [SEP, title + "\n", SEP, "\n"]
|
||||||
|
for k in keys:
|
||||||
|
t = {"VIEW": "vista", "MATERIALIZED VIEW": "vista_mat", "BASE TABLE": "tabla",
|
||||||
|
"EXTERNAL": "externa", "UNKNOWN": "?"}.get(G[k]["type"], "")
|
||||||
|
lines.append(f"[{t:9s}] {k}\n")
|
||||||
|
lines.append(f" {desc_of(k)}\n")
|
||||||
|
lines.append(f" archivo: {relpath_of(k)}\n\n")
|
||||||
|
return "".join(lines)
|
||||||
|
|
||||||
|
idx.append(index_block("1) MARTS FINALES (customer_marts)", marts))
|
||||||
|
idx.append(index_block("2) CAPA INTERMEDIA (clientes_intel)",
|
||||||
|
sorted(k for k in G if k.startswith("clientes_intel."))))
|
||||||
|
idx.append(index_block("3) CADENA DE PRODUCTO",
|
||||||
|
sorted(k for k in G if folder_of(k) == "03_producto")))
|
||||||
|
idx.append(index_block("4) FUENTES BASE",
|
||||||
|
sorted(k for k in G if folder_of(k) == "04_fuentes")))
|
||||||
|
|
||||||
|
w("00_INDICE.txt", "".join(idx))
|
||||||
|
|
||||||
|
# Conteo final
|
||||||
|
n_files = sum(len(files) for _, _, files in os.walk(DEST))
|
||||||
|
print(f"Escrito en: {DEST}")
|
||||||
|
print(f"Archivos .txt generados: {n_files}")
|
||||||
|
print("Estructura:")
|
||||||
|
for root, dirs, files in sorted(os.walk(DEST)):
|
||||||
|
rel = os.path.relpath(root, DEST)
|
||||||
|
if rel == ".":
|
||||||
|
for f in sorted(files):
|
||||||
|
print(f" {f}")
|
||||||
|
else:
|
||||||
|
print(f" {rel}/ ({len(files)} archivos)")
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
"""Genera 05_construccion_clientes_intel/ (SQL CTAS de cada tabla de clientes_intel)
|
||||||
|
y 00b_FUENTES_DE_CLIENTE.txt (mapa fuente-de-cliente -> consulta que la lee).
|
||||||
|
|
||||||
|
Fuente de datos: scratchpad/intel_build.json (SQL de construccion capturado de
|
||||||
|
INFORMATION_SCHEMA.JOBS) y scratchpad/intel_lineage.json (tablas implicadas).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
DEST = "/mnt/c/Users/egutierrez/Desktop/linaje_customer_marts"
|
||||||
|
PROJECT = "autingo-159109"
|
||||||
|
builds = json.load(open("scratchpad/intel_build.json"))
|
||||||
|
lin = json.load(open("scratchpad/intel_lineage.json"))
|
||||||
|
|
||||||
|
# Tablas para las que escribimos el SQL de construccion: las del linaje de customer_marts
|
||||||
|
# + las que leen fuentes de cliente/Salesforce.
|
||||||
|
EXTRA = ["seg_vega_persona", "fact_campana_respuesta__sfnew"]
|
||||||
|
want = sorted(set(lin["intel_involved"]) | set(EXTRA))
|
||||||
|
want = [t for t in want if t in builds] # solo las que tienen SQL capturado
|
||||||
|
|
||||||
|
DESC = {
|
||||||
|
"_persona_records":
|
||||||
|
"IDENTIDAD DEL CLIENTE (nucleo). UNION de 7 fuentes -> normaliza DNI/NIE/CIF, email y "
|
||||||
|
"telefono -> resuelve persona_id (FARM_FINGERPRINT de persona_key) con nivel de confianza. "
|
||||||
|
"AQUI es donde se juntan TPV customers, customers web, OTR, Navision, citaprevia, users y "
|
||||||
|
"Salesforce contacts_latest.",
|
||||||
|
"dim_persona":
|
||||||
|
"Dimension PERSONA final: una fila por persona_id, elegida desde _persona_records "
|
||||||
|
"(prioriza el mejor registro por fuente/confianza) + banderas de contacto.",
|
||||||
|
"dim_vehiculo":
|
||||||
|
"Dimension VEHICULO: una fila por vehiculo (matricula/bastidor) desde TPV vehicles, OTR, "
|
||||||
|
"citaprevia matriculas y calibrado de ano de matricula.",
|
||||||
|
"map_persona_fuente":
|
||||||
|
"Mapeo persona -> fuente(s) de origen (tpv/web/otr/navision/citaprevia/users/salesforce). "
|
||||||
|
"Registra de que sistemas proviene cada persona.",
|
||||||
|
"map_persona_vehiculo":
|
||||||
|
"Mapeo N:N persona <-> vehiculo (quien conduce/posee que coche) desde OTR, TPV vehicleowner "
|
||||||
|
"y citaprevia matriculas.",
|
||||||
|
"map_persona_canal8":
|
||||||
|
"Mapeo persona -> canal8 (canal de entrada del cliente).",
|
||||||
|
"fact_transaccion":
|
||||||
|
"Tabla de HECHOS de transaccion (linea/venta por persona). Base de los marts monetarios.",
|
||||||
|
"fact_visita":
|
||||||
|
"Tabla de HECHOS de visita (visitas del cliente al taller/tienda).",
|
||||||
|
"fact_campana_respuesta":
|
||||||
|
"HECHOS de respuesta a campanas: cruza envios/aperturas/clics/sms de Salesforce con personas.",
|
||||||
|
"fact_campana_respuesta__sfnew":
|
||||||
|
"Variante de fact_campana_respuesta con el esquema nuevo de Salesforce (email_sent/opened/clicked/sms).",
|
||||||
|
"feat_cliente_persona":
|
||||||
|
"Features agregadas por PERSONA (RFM, mix de canal, ticket medio, margen, recencia...).",
|
||||||
|
"feat_cliente_vehiculo":
|
||||||
|
"Features agregadas por VEHICULO.",
|
||||||
|
"seg_cliente_360":
|
||||||
|
"Segmentacion 360 del cliente (segmentos/clusters de negocio).",
|
||||||
|
"seg_vega_persona":
|
||||||
|
"Segmentacion VEGA por persona (contactabilidad/valor); lee fuentes de cliente para calcular "
|
||||||
|
"disponibilidad de contacto.",
|
||||||
|
"seg_cluster_persona":
|
||||||
|
"Clustering de personas (asignacion de cluster) que alimenta la segmentacion.",
|
||||||
|
"reco_acciones":
|
||||||
|
"Recomendaciones / proxima mejor accion (NBA) por cliente.",
|
||||||
|
"data_points_contacto":
|
||||||
|
"Puntos de dato de contacto (email/telefono) consolidados y calidad por persona.",
|
||||||
|
"_margen_rate_producto":
|
||||||
|
"Tasa de margen por producto (auxiliar para features monetarias).",
|
||||||
|
"_plate_year_calib":
|
||||||
|
"Calibrado del ano a partir de la matricula (auxiliar para dim_vehiculo).",
|
||||||
|
"dim_cp_provincia":
|
||||||
|
"Diccionario codigo postal -> provincia/CCAA.",
|
||||||
|
"tipologia_cliente":
|
||||||
|
"Tipologia de cliente (clasificacion de negocio).",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Descripcion corta de cada fuente de cliente.
|
||||||
|
SRC_DESC = {
|
||||||
|
"psql_dcpublic.tpv_customers": "Clientes del TPV (mostrador). Replica Postgres ANJANA (DCPublic).",
|
||||||
|
"psql_dcpublic.customers": "Clientes web (e-commerce). Replica Postgres ANJANA (DCPublic).",
|
||||||
|
"psql_dcpublic.otr_customers": "Clientes de OTR (ordenes de reparacion/taller). Replica Postgres ANJANA.",
|
||||||
|
"psql_dcpublic.users": "Usuarios (cuentas). Replica Postgres ANJANA (DCPublic).",
|
||||||
|
"mssql2022_dbo.anjana_customer": "Cliente de NAVISION (SQL Server 2022, esquema dbo). Campos no_/e_mail/movil/name/post_code.",
|
||||||
|
"salesforce_ew1.contacts_latest": "Contactos de SALESFORCE (ultima version). Dataset en europe-west1.",
|
||||||
|
"salesforce_ew1.email_sent": "Envios de email de Salesforce (Marketing Cloud).",
|
||||||
|
"salesforce_ew1.email_opened": "Aperturas de email de Salesforce.",
|
||||||
|
"salesforce_ew1.email_clicked": "Clics de email de Salesforce.",
|
||||||
|
"salesforce_ew1.sms": "SMS de Salesforce.",
|
||||||
|
"citaprevia_aurphcp.clientes": "Clientes de CITA PREVIA (aurphcp).",
|
||||||
|
"citaprevia_aurphcp.clientes_matriculas": "Matriculas por cliente en cita previa.",
|
||||||
|
"psql_dcpublic.tpv_vehicles_vehicle": "Vehiculos del TPV. Replica Postgres ANJANA.",
|
||||||
|
"psql_dcpublic.tpv_vehicles_vehicleowner": "Propietarios de vehiculo del TPV (N:N). Replica Postgres ANJANA.",
|
||||||
|
}
|
||||||
|
|
||||||
|
CUST_SOURCES = list(SRC_DESC.keys())
|
||||||
|
|
||||||
|
SEP = "=" * 78 + "\n"
|
||||||
|
|
||||||
|
def w(path, text):
|
||||||
|
full = os.path.join(DEST, path)
|
||||||
|
os.makedirs(os.path.dirname(full), exist_ok=True)
|
||||||
|
with open(full, "w", newline="\r\n", encoding="utf-8") as f:
|
||||||
|
f.write(text)
|
||||||
|
|
||||||
|
def build_file(tbl):
|
||||||
|
b = builds[tbl]
|
||||||
|
out = [SEP, f"OBJETO : {PROJECT}.clientes_intel.{tbl}\n",
|
||||||
|
f"TIPO : TABLA BASE construida por {b['stmt']} (se reconstruye periodicamente)\n",
|
||||||
|
f"ULTIMA EJECUCION CAPTURADA: {b['last_run']}\n", SEP,
|
||||||
|
"\nQUE ES\n------\n",
|
||||||
|
textwrap.fill(DESC.get(tbl, "(sin descripcion)"), width=78) + "\n"]
|
||||||
|
if b["refs"]:
|
||||||
|
out.append("\nLEE DE (tablas fuente / intermedias)\n------------------------------------\n")
|
||||||
|
for r in b["refs"]:
|
||||||
|
note = " << FUENTE DE CLIENTE" if r in SRC_DESC else ""
|
||||||
|
out.append(f" - {PROJECT}.{r}{note}\n")
|
||||||
|
out.append("\nSQL DE CONSTRUCCION (copiable)\n------------------------------\n\n")
|
||||||
|
out.append(b["query"].strip() + "\n")
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
for t in want:
|
||||||
|
w(f"05_construccion_clientes_intel/{t}.txt", build_file(t))
|
||||||
|
|
||||||
|
# 00b_FUENTES_DE_CLIENTE.txt
|
||||||
|
f = [SEP, "FUENTES DE CLIENTE -> QUE CONSULTA DE clientes_intel LAS USA\n", SEP,
|
||||||
|
"\nResponde a: de donde salen los clientes (TPV, web, OTR, Navision, Salesforce, cita\n"
|
||||||
|
"previa) y en que consulta se juntan. El punto de union de identidades es\n"
|
||||||
|
"_persona_records (ver 05_construccion_clientes_intel/_persona_records.txt).\n\n"]
|
||||||
|
|
||||||
|
f.append(SEP + "RESUMEN: LO QUE PEDISTE\n" + SEP + "\n")
|
||||||
|
mapping = [
|
||||||
|
("TPV customers", "psql_dcpublic.tpv_customers"),
|
||||||
|
("customers (web)", "psql_dcpublic.customers"),
|
||||||
|
("customers (OTR / taller)", "psql_dcpublic.otr_customers"),
|
||||||
|
("users", "psql_dcpublic.users"),
|
||||||
|
("customer de NAVISION", "mssql2022_dbo.anjana_customer"),
|
||||||
|
("SALESFORCE (contactos)", "salesforce_ew1.contacts_latest"),
|
||||||
|
]
|
||||||
|
for label, src in mapping:
|
||||||
|
f.append(f" {label:26s} -> {PROJECT}.{src}\n")
|
||||||
|
f.append("\n SI: tenemos Salesforce. El dataset es `salesforce_ew1` (europe-west1):\n"
|
||||||
|
" contactos en contacts_latest; marketing en email_sent/opened/clicked y sms.\n\n")
|
||||||
|
|
||||||
|
for src in CUST_SOURCES:
|
||||||
|
consumers = sorted(t for t, b in builds.items() if src in b["refs"])
|
||||||
|
f.append(SEP)
|
||||||
|
f.append(f"{PROJECT}.{src}\n")
|
||||||
|
f.append(SEP)
|
||||||
|
f.append(f" {SRC_DESC[src]}\n")
|
||||||
|
f.append(" La leen estas tablas de clientes_intel (con su SQL en 05_construccion_...):\n")
|
||||||
|
if consumers:
|
||||||
|
for t in consumers:
|
||||||
|
star = " [SQL disponible]" if t in want else ""
|
||||||
|
f.append(f" - {t} ({builds[t]['stmt']}){star}\n")
|
||||||
|
else:
|
||||||
|
f.append(" (ninguna la referencia directamente)\n")
|
||||||
|
f.append("\n")
|
||||||
|
|
||||||
|
w("00b_FUENTES_DE_CLIENTE.txt", "".join(f))
|
||||||
|
|
||||||
|
print("Generado:")
|
||||||
|
print(f" 05_construccion_clientes_intel/ -> {len(want)} archivos con SQL de construccion")
|
||||||
|
print(f" 00b_FUENTES_DE_CLIENTE.txt")
|
||||||
|
print("\nTablas con SQL de construccion escrito:")
|
||||||
|
for t in want:
|
||||||
|
print(f" - {t}")
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
"""Genera 00c_VERIFICACION.txt (chequeo de completitud del linaje) y
|
||||||
|
06_otros_outputs_clientes_intel/ (SQL de las tablas de clientes_intel que NO acaban
|
||||||
|
en customer_marts, para no dejar ninguna atras).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
DEST = "/mnt/c/Users/egutierrez/Desktop/linaje_customer_marts"
|
||||||
|
PROJECT = "autingo-159109"
|
||||||
|
builds = json.load(open("scratchpad/intel_build.json"))
|
||||||
|
lin = json.load(open("scratchpad/intel_lineage.json"))
|
||||||
|
involved = set(lin["intel_involved"])
|
||||||
|
|
||||||
|
# Catalogo completo de clientes_intel (40 objetos) reconstruido: involved + leftovers conocidos.
|
||||||
|
LEFTOVER = [
|
||||||
|
"_presupuesto_persona", "_veh_cluster_feat", "_veh_tec_feat", "audit_persona_divergencias",
|
||||||
|
"calidad_email_snapshot", "f0_audit_keys", "fact_impacto_campana", "map_mutualista_particular",
|
||||||
|
"reco_promo_personalizada", "reco_promo_segmento", "rpt_campana", "rpt_campana_lift",
|
||||||
|
"rpt_campana_usuario", "rpt_impacto_persona", "seg_audiencia", "seg_vega_persona",
|
||||||
|
"sf_contact_map", "tipologia_cliente_resumen", "veh_cluster",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Clasificacion por proposito (a donde va cada leftover).
|
||||||
|
CATEGORY = {
|
||||||
|
"rpt_campana": "Informe de campanas (BI / dashboards de marketing)",
|
||||||
|
"rpt_campana_lift": "Informe de campanas: lift (BI / dashboards)",
|
||||||
|
"rpt_campana_usuario": "Informe de campanas por usuario (BI / dashboards)",
|
||||||
|
"rpt_impacto_persona": "Informe de impacto por persona (BI / dashboards)",
|
||||||
|
"fact_impacto_campana": "Hechos de impacto de campana (base de los informes)",
|
||||||
|
"reco_promo_personalizada": "Recomendacion de promo personalizada (activacion)",
|
||||||
|
"reco_promo_segmento": "Recomendacion de promo por segmento (activacion)",
|
||||||
|
"seg_audiencia": "Audiencias para activacion (probable push a Salesforce/Marketing)",
|
||||||
|
"sf_contact_map": "Mapa de contactos Salesforce (sincronizacion de IDs)",
|
||||||
|
"audit_persona_divergencias": "Auditoria de calidad: divergencias en resolucion de persona",
|
||||||
|
"calidad_email_snapshot": "Auditoria de calidad: snapshot de emails",
|
||||||
|
"f0_audit_keys": "Auditoria de claves (control interno del pipeline)",
|
||||||
|
"_presupuesto_persona": "Auxiliar: presupuestos por persona (interim)",
|
||||||
|
"_veh_cluster_feat": "Auxiliar: features para clustering de vehiculo (interim)",
|
||||||
|
"_veh_tec_feat": "Auxiliar: features tecnicas de vehiculo (interim)",
|
||||||
|
"veh_cluster": "Clustering de vehiculo (resultado; no lo usan los marts hoy)",
|
||||||
|
"tipologia_cliente_resumen": "Resumen de tipologia de cliente (BI)",
|
||||||
|
"map_mutualista_particular": "Vista auxiliar: mapa mutualista/particular",
|
||||||
|
"seg_vega_persona": "Segmentacion VEGA por persona (contactabilidad; lee fuentes de cliente)",
|
||||||
|
}
|
||||||
|
|
||||||
|
SEP = "=" * 78 + "\n"
|
||||||
|
|
||||||
|
def w(path, text):
|
||||||
|
full = os.path.join(DEST, path)
|
||||||
|
os.makedirs(os.path.dirname(full), exist_ok=True)
|
||||||
|
with open(full, "w", newline="\r\n", encoding="utf-8") as f:
|
||||||
|
f.write(text)
|
||||||
|
|
||||||
|
# --- 06: SQL de los leftovers que tengan build capturado ---
|
||||||
|
written = []
|
||||||
|
for t in LEFTOVER:
|
||||||
|
b = builds.get(t)
|
||||||
|
if not b:
|
||||||
|
continue
|
||||||
|
out = [SEP, f"OBJETO : {PROJECT}.clientes_intel.{t}\n",
|
||||||
|
f"TIPO : {b['stmt']} (NO alimenta customer_marts)\n",
|
||||||
|
f"ULTIMA EJECUCION CAPTURADA: {b['last_run']}\n", SEP,
|
||||||
|
"\nQUE ES / A DONDE VA\n-------------------\n",
|
||||||
|
textwrap.fill(CATEGORY.get(t, "(sin clasificar)"), width=78) + "\n"]
|
||||||
|
if b["refs"]:
|
||||||
|
out.append("\nLEE DE\n------\n")
|
||||||
|
for r in b["refs"]:
|
||||||
|
out.append(f" - {PROJECT}.{r}\n")
|
||||||
|
out.append("\nSQL DE CONSTRUCCION (copiable)\n------------------------------\n\n")
|
||||||
|
out.append(b["query"].strip() + "\n")
|
||||||
|
w(f"06_otros_outputs_clientes_intel/{t}.txt", "".join(out))
|
||||||
|
written.append(t)
|
||||||
|
|
||||||
|
# --- 00c: verificacion de completitud ---
|
||||||
|
v = [SEP, "VERIFICACION DE COMPLETITUD DEL LINAJE\n", SEP, "\n"]
|
||||||
|
v.append("PREGUNTA: todo esto acaba en customer_marts? Comprobado.\n\n")
|
||||||
|
v.append("""RESPUESTA CORTA
|
||||||
|
---------------
|
||||||
|
La cadena customer_marts -> fuentes esta COMPLETA (todas las referencias resueltas,
|
||||||
|
0 tablas sin identificar). PERO customer_marts NO es el unico destino: es UNO de los
|
||||||
|
consumidores de la capa clientes_intel.
|
||||||
|
|
||||||
|
- clientes_intel tiene 40 objetos.
|
||||||
|
- 21 de ellos alimentan (directa o indirectamente) las 14 vistas de customer_marts.
|
||||||
|
- 19 NO van a customer_marts: son OTRAS salidas del mismo pipeline (informes de
|
||||||
|
campana, recomendaciones de promo, audiencias, auditorias, auxiliares).
|
||||||
|
|
||||||
|
El unico dataset MODELADO que lee clientes_intel es customer_marts. El resto de lo que
|
||||||
|
lee clientes_intel y customer_marts son consultas de BI / ad-hoc (tablas temporales
|
||||||
|
_hexhash / anon...), es decir Metabase u otros lo consumen directamente. En ese sentido
|
||||||
|
customer_marts SI es terminal en el modelo (aguas abajo solo hay BI).
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
v.append(SEP + "1) LAS 21 TABLAS DE clientes_intel QUE SI ALIMENTAN customer_marts\n" + SEP + "\n")
|
||||||
|
for t in sorted(involved):
|
||||||
|
b = builds.get(t, {})
|
||||||
|
v.append(f" - {t} ({b.get('stmt','(sin job)')})\n")
|
||||||
|
|
||||||
|
v.append("\n" + SEP + "2) LAS 19 TABLAS DE clientes_intel QUE NO VAN A customer_marts\n" + SEP + "\n")
|
||||||
|
v.append(" (SQL de cada una en 06_otros_outputs_clientes_intel/)\n\n")
|
||||||
|
for t in LEFTOVER:
|
||||||
|
sql_note = "" if t in written else " [sin SQL de job capturado]"
|
||||||
|
v.append(f" - {t:28s} {CATEGORY.get(t,'')}{sql_note}\n")
|
||||||
|
|
||||||
|
v.append("\n" + SEP + "3) FUENTES BASE ALCANZADAS (fin del linaje)\n" + SEP + "\n")
|
||||||
|
v.append(" Fuera de clientes_intel, el pipeline lee de:\n\n")
|
||||||
|
for s in sorted(lin["external_sources"]):
|
||||||
|
v.append(f" - {PROJECT}.{s}\n")
|
||||||
|
|
||||||
|
v.append("\n" + SEP + "4) NOTAS DE COBERTURA\n" + SEP + "\n")
|
||||||
|
v.append(""" - score_clv y seg_cluster_vehiculo: usadas por customer_marts pero sin CTAS reciente
|
||||||
|
en el historial de jobs (son modelos ML / cargas antiguas). Su esquema esta en
|
||||||
|
02_intermedio_clientes_intel/; no hay un SQL de un solo job que las reconstruya.
|
||||||
|
- El SQL de construccion se tomo del ULTIMO job exitoso de cada tabla
|
||||||
|
(INFORMATION_SCHEMA.JOBS, region europe-west1, ventana 120 dias). Si una tabla se
|
||||||
|
reconstruye con otra logica fuera de esa ventana, no se captura aqui.
|
||||||
|
- customer_marts: 14 vistas = el dataset entero (no falta ninguna).
|
||||||
|
""")
|
||||||
|
|
||||||
|
w("00c_VERIFICACION.txt", "".join(v))
|
||||||
|
|
||||||
|
print(f"06_otros_outputs_clientes_intel/ -> {len(written)} archivos")
|
||||||
|
print("00c_VERIFICACION.txt -> escrito")
|
||||||
|
print("\nLeftovers sin SQL capturado:", [t for t in LEFTOVER if t not in written] or "ninguno")
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"intel_involved": [
|
||||||
|
"_margen_rate_producto",
|
||||||
|
"_persona_records",
|
||||||
|
"_plate_year_calib",
|
||||||
|
"data_points_contacto",
|
||||||
|
"dim_cp_provincia",
|
||||||
|
"dim_persona",
|
||||||
|
"dim_vehiculo",
|
||||||
|
"fact_campana_respuesta",
|
||||||
|
"fact_transaccion",
|
||||||
|
"fact_visita",
|
||||||
|
"feat_cliente_persona",
|
||||||
|
"feat_cliente_vehiculo",
|
||||||
|
"map_persona_canal8",
|
||||||
|
"map_persona_fuente",
|
||||||
|
"map_persona_vehiculo",
|
||||||
|
"reco_acciones",
|
||||||
|
"score_clv",
|
||||||
|
"seg_cliente_360",
|
||||||
|
"seg_cluster_persona",
|
||||||
|
"seg_cluster_vehiculo",
|
||||||
|
"tipologia_cliente"
|
||||||
|
],
|
||||||
|
"external_sources": [
|
||||||
|
"anjana_bi_amg.margenes_mat",
|
||||||
|
"citaprevia_aurphcp.clientes",
|
||||||
|
"citaprevia_aurphcp.clientes_matriculas",
|
||||||
|
"claude_bi.churn_scores_current",
|
||||||
|
"claude_bi.conversion_cqg_base_mat",
|
||||||
|
"claude_bi.todos_datos_lineas_mat",
|
||||||
|
"mssql2022_dbo.anjana_customer",
|
||||||
|
"ontologia.aurgiCitas_mat",
|
||||||
|
"psql_dcpublic.call_transactions",
|
||||||
|
"psql_dcpublic.car_makes",
|
||||||
|
"psql_dcpublic.car_model_families",
|
||||||
|
"psql_dcpublic.car_models",
|
||||||
|
"psql_dcpublic.car_versions",
|
||||||
|
"psql_dcpublic.customers",
|
||||||
|
"psql_dcpublic.otr_customers",
|
||||||
|
"psql_dcpublic.otr_vehicles",
|
||||||
|
"psql_dcpublic.tecrmi_license_plates",
|
||||||
|
"psql_dcpublic.tpv_customers",
|
||||||
|
"psql_dcpublic.tpv_vehicles_vehicle",
|
||||||
|
"psql_dcpublic.tpv_vehicles_vehicleowner",
|
||||||
|
"psql_dcpublic.users",
|
||||||
|
"salesforce_ew1.contacts_latest",
|
||||||
|
"salesforce_ew1.email_clicked",
|
||||||
|
"salesforce_ew1.email_opened",
|
||||||
|
"salesforce_ew1.email_sent",
|
||||||
|
"salesforce_ew1.sms"
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,106 @@
|
|||||||
|
"""Traza la construccion de clientes_intel: para cada tabla, recupera el SQL del ultimo
|
||||||
|
job que la escribio (INFORMATION_SCHEMA.JOBS) + sus referenced_tables, y recorre hacia
|
||||||
|
atras hasta las tablas fuente (TPV, customers, users, Navision, Salesforce).
|
||||||
|
|
||||||
|
Vuelca todo a scratchpad/intel_build.json.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
import google.auth
|
||||||
|
from google.cloud import bigquery
|
||||||
|
|
||||||
|
PROJECT = "autingo-159109"
|
||||||
|
REGION = "region-europe-west1"
|
||||||
|
|
||||||
|
creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/bigquery"])
|
||||||
|
creds = creds.with_quota_project(None)
|
||||||
|
c = bigquery.Client(project=PROJECT, credentials=creds)
|
||||||
|
|
||||||
|
# Ultimo job por tabla destino en clientes_intel: query + referenced_tables + stmt.
|
||||||
|
sql = f"""
|
||||||
|
WITH j AS (
|
||||||
|
SELECT
|
||||||
|
dest.table_id AS tbl,
|
||||||
|
query,
|
||||||
|
statement_type AS stmt,
|
||||||
|
creation_time,
|
||||||
|
ARRAY(
|
||||||
|
SELECT AS STRUCT rt.project_id, rt.dataset_id, rt.table_id
|
||||||
|
FROM UNNEST(referenced_tables) rt
|
||||||
|
) AS refs,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY dest.table_id ORDER BY creation_time DESC) AS rn
|
||||||
|
FROM `{PROJECT}`.`{REGION}`.INFORMATION_SCHEMA.JOBS_BY_PROJECT,
|
||||||
|
UNNEST([destination_table]) dest
|
||||||
|
WHERE dest.dataset_id = 'clientes_intel'
|
||||||
|
AND state = 'DONE' AND error_result IS NULL
|
||||||
|
AND statement_type IS NOT NULL
|
||||||
|
AND creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 120 DAY)
|
||||||
|
)
|
||||||
|
SELECT tbl, query, stmt, creation_time, refs FROM j WHERE rn = 1
|
||||||
|
ORDER BY tbl
|
||||||
|
"""
|
||||||
|
|
||||||
|
builds = {}
|
||||||
|
for r in c.query(sql).result():
|
||||||
|
refs = []
|
||||||
|
for rt in r.refs:
|
||||||
|
refs.append(f"{rt['dataset_id']}.{rt['table_id']}")
|
||||||
|
builds[r.tbl] = {
|
||||||
|
"query": r.query or "",
|
||||||
|
"stmt": r.stmt,
|
||||||
|
"last_run": str(r.creation_time),
|
||||||
|
"refs": sorted(set(x for x in refs if not x.endswith(f".{r.tbl}"))),
|
||||||
|
}
|
||||||
|
|
||||||
|
json.dump(builds, open("scratchpad/intel_build.json", "w"), indent=2, ensure_ascii=False)
|
||||||
|
print(f"tablas clientes_intel con SQL de construccion capturado: {len(builds)}\n")
|
||||||
|
|
||||||
|
# Recursion desde las 12 tablas usadas por customer_marts.
|
||||||
|
SEED = [
|
||||||
|
"dim_persona", "dim_vehiculo", "fact_transaccion", "fact_campana_respuesta",
|
||||||
|
"feat_cliente_persona", "feat_cliente_vehiculo", "seg_cliente_360", "score_clv",
|
||||||
|
"reco_acciones", "map_persona_canal8", "map_persona_fuente", "map_persona_vehiculo",
|
||||||
|
]
|
||||||
|
intel_involved = set()
|
||||||
|
external_sources = set()
|
||||||
|
stack = list(SEED)
|
||||||
|
while stack:
|
||||||
|
t = stack.pop()
|
||||||
|
if t in intel_involved:
|
||||||
|
continue
|
||||||
|
intel_involved.add(t)
|
||||||
|
b = builds.get(t)
|
||||||
|
if not b:
|
||||||
|
continue
|
||||||
|
for ref in b["refs"]:
|
||||||
|
ds, tbl = ref.split(".", 1)
|
||||||
|
if ds == "clientes_intel":
|
||||||
|
if tbl not in intel_involved:
|
||||||
|
stack.append(tbl)
|
||||||
|
else:
|
||||||
|
external_sources.add(ref)
|
||||||
|
|
||||||
|
print("== tablas clientes_intel implicadas en el linaje de customer_marts ==")
|
||||||
|
for t in sorted(intel_involved):
|
||||||
|
b = builds.get(t, {})
|
||||||
|
print(f" {t:26s} {b.get('stmt','(sin job)')}")
|
||||||
|
|
||||||
|
print("\n== FUENTES EXTERNAS (fuera de clientes_intel) usadas por el pipeline ==")
|
||||||
|
for s in sorted(external_sources):
|
||||||
|
print(f" {s}")
|
||||||
|
|
||||||
|
# Marcar las fuentes de CLIENTE que pide el usuario.
|
||||||
|
KEYS = ["customer", "customers", "cliente", "user", "usuario", "tpv", "salesforce",
|
||||||
|
"sf_", "contact", "mkt_cloud", "persona"]
|
||||||
|
print("\n== fuentes que parecen de CLIENTE/usuario ==")
|
||||||
|
for s in sorted(external_sources):
|
||||||
|
low = s.lower()
|
||||||
|
if any(k in low for k in KEYS):
|
||||||
|
print(f" {s}")
|
||||||
|
|
||||||
|
json.dump({
|
||||||
|
"intel_involved": sorted(intel_involved),
|
||||||
|
"external_sources": sorted(external_sources),
|
||||||
|
}, open("scratchpad/intel_lineage.json", "w"), indent=2, ensure_ascii=False)
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
"""Traza el linaje recursivo de las vistas de customer_marts hasta las tablas fuente.
|
||||||
|
|
||||||
|
Para cada objeto: obtiene su tipo (VIEW/BASE TABLE/EXTERNAL/MATERIALIZED VIEW) y su DDL
|
||||||
|
via INFORMATION_SCHEMA.TABLES, extrae las referencias a otras tablas del DDL y recurre
|
||||||
|
sobre las que son vistas. Vuelca el grafo completo a un JSON en scratchpad.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
import google.auth
|
||||||
|
from google.cloud import bigquery
|
||||||
|
|
||||||
|
PROJECT = "autingo-159109"
|
||||||
|
|
||||||
|
creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/bigquery"])
|
||||||
|
creds = creds.with_quota_project(None)
|
||||||
|
client = bigquery.Client(project=PROJECT, credentials=creds)
|
||||||
|
|
||||||
|
# Cache de metadata por dataset: {dataset: {table_name: {"type":..., "ddl":...}}}
|
||||||
|
dataset_cache: dict[str, dict] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def load_dataset(dataset: str) -> dict:
|
||||||
|
"""Carga todas las tablas/vistas de un dataset (una query por dataset)."""
|
||||||
|
if dataset in dataset_cache:
|
||||||
|
return dataset_cache[dataset]
|
||||||
|
result: dict[str, dict] = {}
|
||||||
|
try:
|
||||||
|
sql = f"""
|
||||||
|
SELECT table_name, table_type, ddl
|
||||||
|
FROM `{PROJECT}`.`{dataset}`.INFORMATION_SCHEMA.TABLES
|
||||||
|
"""
|
||||||
|
for r in client.query(sql).result():
|
||||||
|
result[r.table_name] = {"type": r.table_type, "ddl": r.ddl or ""}
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f" [warn] no se pudo leer dataset {dataset}: {e}", file=sys.stderr)
|
||||||
|
dataset_cache[dataset] = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# En el DDL que emite INFORMATION_SCHEMA, las referencias a otras tablas SIEMPRE van
|
||||||
|
# entre backticks y totalmente cualificadas: `proyecto.dataset.tabla`. Los alias de
|
||||||
|
# CTE/JOIN (dp, fcp, f...) nunca llevan backticks, asi que restringiendo a lo que hay
|
||||||
|
# entre backticks eliminamos todo el ruido.
|
||||||
|
BACKTICK_RE = re.compile(r"`([^`]+)`")
|
||||||
|
# Variante con cada parte en su propio backtick: `proj`.`dataset`.`tabla`
|
||||||
|
MULTIPART_RE = re.compile(
|
||||||
|
r"`([A-Za-z0-9_-]+)`\.`([A-Za-z0-9_-]+)`(?:\.`([A-Za-z0-9_-]+)`)?"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _norm(proj: str, ds: str, tbl: str) -> tuple[str, str] | None:
|
||||||
|
if ds.upper() == "INFORMATION_SCHEMA" or tbl.upper() == "INFORMATION_SCHEMA":
|
||||||
|
return None
|
||||||
|
return (ds, tbl)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_refs(ddl: str) -> set[tuple[str, str]]:
|
||||||
|
"""Devuelve el conjunto de (dataset, table) referenciados en el cuerpo del DDL.
|
||||||
|
|
||||||
|
Se queda con el SELECT (tras el primer 'AS') para no capturar el nombre del propio objeto.
|
||||||
|
"""
|
||||||
|
body = ddl
|
||||||
|
m = re.search(r"\bAS\b", ddl, flags=re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
body = ddl[m.end():]
|
||||||
|
|
||||||
|
refs: set[tuple[str, str]] = set()
|
||||||
|
|
||||||
|
# Estilo `proyecto.dataset.tabla` (todo en un backtick).
|
||||||
|
for tok in BACKTICK_RE.findall(body):
|
||||||
|
parts = [p for p in tok.split(".") if p]
|
||||||
|
if len(parts) == 3:
|
||||||
|
r = _norm(parts[0], parts[1], parts[2])
|
||||||
|
elif len(parts) == 2:
|
||||||
|
r = _norm(PROJECT, parts[0], parts[1])
|
||||||
|
else:
|
||||||
|
r = None
|
||||||
|
if r:
|
||||||
|
refs.add(r)
|
||||||
|
|
||||||
|
# Estilo `proj`.`dataset`.`tabla` (parte por backtick, 3 partes cualificadas).
|
||||||
|
# OJO: `alias`.`columna` (2 partes con cada parte en su propio backtick) es una
|
||||||
|
# referencia a columna, NO a tabla — se descarta exigiendo las 3 partes.
|
||||||
|
for mt in MULTIPART_RE.finditer(body):
|
||||||
|
g1, g2, g3 = mt.group(1), mt.group(2), mt.group(3)
|
||||||
|
if g3:
|
||||||
|
r = _norm(g1, g2, g3)
|
||||||
|
if r:
|
||||||
|
refs.add(r)
|
||||||
|
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
graph: dict[str, dict] = {} # key "dataset.table" -> {type, ddl, refs:[...]}
|
||||||
|
visited: set[str] = set()
|
||||||
|
|
||||||
|
|
||||||
|
def visit(dataset: str, table: str, depth: int = 0):
|
||||||
|
key = f"{dataset}.{table}"
|
||||||
|
if key in visited:
|
||||||
|
return
|
||||||
|
visited.add(key)
|
||||||
|
meta = load_dataset(dataset).get(table)
|
||||||
|
if meta is None:
|
||||||
|
graph[key] = {"type": "UNKNOWN", "ddl": "", "refs": [], "depth": depth}
|
||||||
|
return
|
||||||
|
ddl = meta["ddl"]
|
||||||
|
ttype = meta["type"]
|
||||||
|
refs: list[str] = []
|
||||||
|
if ttype in ("VIEW", "MATERIALIZED VIEW"):
|
||||||
|
for ds, tbl in sorted(extract_refs(ddl)):
|
||||||
|
# Evitar auto-referencia
|
||||||
|
if ds == dataset and tbl == table:
|
||||||
|
continue
|
||||||
|
refs.append(f"{ds}.{tbl}")
|
||||||
|
graph[key] = {"type": ttype, "ddl": ddl, "refs": refs, "depth": depth}
|
||||||
|
for ref in refs:
|
||||||
|
rds, rtbl = ref.split(".", 1)
|
||||||
|
visit(rds, rtbl, depth + 1)
|
||||||
|
|
||||||
|
|
||||||
|
# Semillas: las 14 vistas de customer_marts.
|
||||||
|
SEEDS = [
|
||||||
|
"customer_brand_affinity", "customer_category_spend", "customer_channel",
|
||||||
|
"customer_contactability", "customer_monetary", "customer_payment_method",
|
||||||
|
"customer_predictive", "customer_product", "customer_profile",
|
||||||
|
"customer_promo_tolerance", "customer_promo_usage", "customer_store_spend",
|
||||||
|
"customer_temporal", "customer_vehicles",
|
||||||
|
]
|
||||||
|
for s in SEEDS:
|
||||||
|
visit("customer_marts", s, 0)
|
||||||
|
|
||||||
|
out = {
|
||||||
|
"project": PROJECT,
|
||||||
|
"seeds": [f"customer_marts.{s}" for s in SEEDS],
|
||||||
|
"graph": graph,
|
||||||
|
}
|
||||||
|
with open("scratchpad/lineage_graph.json", "w") as f:
|
||||||
|
json.dump(out, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Resumen
|
||||||
|
n_view = sum(1 for v in graph.values() if v["type"] in ("VIEW", "MATERIALIZED VIEW"))
|
||||||
|
n_base = sum(1 for v in graph.values() if v["type"] == "BASE TABLE")
|
||||||
|
n_ext = sum(1 for v in graph.values() if v["type"] == "EXTERNAL")
|
||||||
|
n_unk = sum(1 for v in graph.values() if v["type"] == "UNKNOWN")
|
||||||
|
print(f"objetos totales: {len(graph)} vistas: {n_view} base: {n_base} external: {n_ext} desconocidos: {n_unk}")
|
||||||
|
print("\n== objetos por dataset ==")
|
||||||
|
by_ds: dict[str, int] = {}
|
||||||
|
for k in graph:
|
||||||
|
ds = k.split(".", 1)[0]
|
||||||
|
by_ds[ds] = by_ds.get(ds, 0) + 1
|
||||||
|
for ds, n in sorted(by_ds.items(), key=lambda x: -x[1]):
|
||||||
|
print(f" {n:3d} {ds}")
|
||||||
Reference in New Issue
Block a user