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