"""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/.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)")