En el mundo de hoy, el fiabilidad de las soluciones de datos lo es todo. Cuando construimos paneles e informes, uno espera que los números reflejados allí sean correctos y actualizados. Según estos números, se dibujan ideas y se toman medidas. Por cualquier razón imprevista, si los paneles están rotos o si los números son incorrectos, entonces se convierte en un pelea de fuego para arreglar todo. Si los problemas no se solucionan en el tiempo, entonces daña el confianza colocado en el equipo de datos y sus soluciones.

Pero, ¿por qué se romperían los paneles o tendrían números equivocados? Si el tablero se construyó correctamente la primera vez, entonces el 99% del tiempo el problema proviene de los datos que alimentan los paneles, desde el almacén de datos. Algunos escenarios posibles son:

  • Pocas tuberías ETL fallaron, por lo que los nuevos datos aún no están en
  • Una mesa se reemplaza por otra nueva
  • Algunas columnas en la tabla se dejan caer o renombran
  • Los esquemas en el almacén de datos han cambiado
  • Y muchos más.

Todavía existe la posibilidad de que el problema esté en el sitio de Tableau, pero en mi experiencia, la mayoría de las veces, siempre se debe a algunos cambios en el almacén de datos. Aunque sabemos la causa raíz, no siempre es sencillo comenzar a trabajar en una solución. Hay No hay lugar central donde puede verificar qué fuentes de datos Tableau dependen de tablas específicas. Si tienes el cuadro Gestión de datos complemento, podría ayudar, pero por lo que sé, es difícil encontrar dependencias de consultas SQL personalizadas utilizadas en fuentes de datos.

Sin embargo, el complemento es demasiado costoso y la mayoría de las empresas no lo tienen. El verdadero dolor comienza cuando tienes que pasar por todas las fuentes de datos manualmente para comenzar a arreglarlo. Además de él, tiene una cadena de usuarios en su cabeza esperando impaciente un fijación rápida. La solución en sí podría no ser difícil, solo sería un tiempo.

¿Y si pudiéramos anticipar estos problemas y Identificar fuentes de datos afectadas antes de que alguien note un problema? ¿No sería eso genial? Bueno, ahora hay una manera con el cuadro API de metadatos. El Metadatos API usa GraphQL, un lenguaje de consulta para API que devuelve solo los datos que le interesan. Para obtener más información sobre lo que es posible con GraphQL, consulte Graphql.org.

En esta publicación de blog, te mostraré cómo conectarte al API de metadatos de Tableau usando PitónS Tableau Server Client (TSC) Biblioteca para identificar de manera proactiva Fuentes de datos que usan tablas específicas, para que pueda actuar rápido antes de que surjan problemas. Una vez que sepa qué fuentes de datos Tableau se ven afectadas por una tabla específica, puede realizar algunas actualizaciones usted mismo o alertar a los propietarios de esas fuentes de datos sobre los próximos cambios para que puedan estar preparados para ello.

Conectarse a la API de metadatos de Tableau

Vamos a conectarnos al Cuadro Servidor usando TSC. ¡Necesitamos importar todas las bibliotecas que necesitaríamos para el ejercicio!

### Import all required libraries
import tableauserverclient as t
import pandas as pd
import json
import ast
import re

Para conectarse a la API de metadatos, primero deberá crear un token de acceso personal en la configuración de su cuenta de Tableau. Luego actualice el <API_TOKEN_NAME> Y <TOKEN_KEY> con el token que acabas de crear. Actualizar también <YOUR_SITE> con su sitio de Tableau. Si la conexión se establece correctamente, se imprimirá «conectado» en la ventana de salida.

### Connect to Tableau server using personal access token
tableau_auth = t.PersonalAccessTokenAuth("<API_TOKEN_NAME>", "<TOKEN_KEY>", 
                                           site_id="<YOUR_SITE>")
server = t.Server("https://dub01.online.tableau.com/", use_server_version=True)

with server.auth.sign_in(tableau_auth):
        print("Connected")

Ahora obtengamos una lista de todas las fuentes de datos que se publican en su sitio. Hay muchos atributos que puede obtener, pero para el caso de uso actual, mantengamos simple y solo obtenga la ID, el nombre y la información de contacto del propietario para cada fuente de datos. Esta será nuestra lista maestra a la que agregaremos toda la otra información.

############### Get all the list of data sources on your Site

all_datasources_query = """ {
  publishedDatasources {
    name
    id
    owner {
    name
    email
    }
  }
}"""
with server.auth.sign_in(tableau_auth):
    result = server.metadata.query(
        all_datasources_query
    )

Dado que quiero que este blog se centre en cómo identificar proactivamente qué fuentes de datos se ven afectadas por una tabla específica, no entraré en los matices de la API de metadatos. Para comprender mejor cómo funciona la consulta, puede consultar un cuadro muy detallado. Documentación de la API de metadatos.

Una cosa a tener en cuenta es que la API de metadatos devuelve datos en formato JSON. Dependiendo de lo que esté consultando, terminará con múltiples listas de JSON anidadas y puede ser muy difícil convertir esto en un marco de datos PANDAS. Para la consulta de metadatos anterior, terminará con un resultado que le gustaría a continuación (estos son datos simulados solo para darle una idea de cómo se ve la salida):

{
  "data": {
    "publishedDatasources": [
      {
        "name": "Sales Performance DataSource",
        "id": "f3b1a2c4-1234-5678-9abc-1234567890ab",
        "owner": {
          "name": "Alice Johnson",
          "email": "[email protected]"
        }
      },
      {
        "name": "Customer Orders DataSource",
        "id": "a4d2b3c5-2345-6789-abcd-2345678901bc",
        "owner": {
          "name": "Bob Smith",
          "email": "[email protected]"
        }
      },
      {
        "name": "Product Returns and Profitability",
        "id": "c5e3d4f6-3456-789a-bcde-3456789012cd",
        "owner": {
          "name": "Alice Johnson",
          "email": "[email protected]"
        }
      },
      {
        "name": "Customer Segmentation Analysis",
        "id": "d6f4e5a7-4567-89ab-cdef-4567890123de",
        "owner": {
          "name": "Charlie Lee",
          "email": "[email protected]"
        }
      },
      {
        "name": "Regional Sales Trends (Custom SQL)",
        "id": "e7a5f6b8-5678-9abc-def0-5678901234ef",
        "owner": {
          "name": "Bob Smith",
          "email": "[email protected]"
        }
      }
    ]
  }
}

Necesitamos convertir esta respuesta JSON en un marco de datos para que sea fácil trabajar. Tenga en cuenta que necesitamos extraer el nombre y el correo electrónico del propietario desde dentro del objeto del propietario.

### We need to convert the response into dataframe for easy data manipulation

col_names = result['data']['publishedDatasources'][0].keys()
master_df = pd.DataFrame(columns=col_names)

for i in result['data']['publishedDatasources']:
    tmp_dt = {k:v for k,v in i.items()}
    master_df = pd.concat([master_df, pd.DataFrame.from_dict(tmp_dt, orient='index').T])

# Extract the owner name and email from the owner object
master_df['owner_name'] = master_df['owner'].apply(lambda x: x.get('name') if isinstance(x, dict) else None)
master_df['owner_email'] = master_df['owner'].apply(lambda x: x.get('email') if isinstance(x, dict) else None)

master_df.reset_index(inplace=True)
master_df.drop(['index','owner'], axis=1, inplace=True)
print('There are ', master_df.shape[0] , ' datasources in your site')

Así es como la estructura de master_df se vería como:

Ejemplo de salida del código

Una vez que tenemos la lista principal lista, podemos seguir adelante y comenzar a obtener los nombres de las tablas incrustadas en las fuentes de datos. Si es un usuario ávido de Tableau, sabe que hay dos formas de seleccionar tablas en una fuente de datos Tableau: una es elegir directamente las tablas y establecer una relación entre ellas y la otra es utilizar una consulta SQL personalizada con una o más tablas para lograr una nueva tabla resultante. Por lo tanto, debemos abordar ambos casos.

Procesamiento de tablas de consultas SQL personalizadas

A continuación se muestra la consulta para obtener la lista de todos los SQL personalizados utilizados en el sitio junto con sus fuentes de datos. Observe que he filtrado la lista para obtener solo las primeras 500 consultas SQL personalizadas. En caso de que haya más en su organización, tendrá que usar un desplazamiento para obtener el siguiente conjunto de consultas SQL personalizadas. También hay una opción de usar el método de cursor en la paginación cuando desea obtener una gran lista de resultados (consulte aquí). En aras de la simplicidad, solo uso el método de compensación como sé, ya que hay menos de 500 consultas SQL personalizadas utilizadas en el sitio.

# Get the data sources and the table names from all the custom sql queries used on your Site

custom_table_query = """  {
  customSQLTablesConnection(first: 500){
    nodes {
        id
        name
        downstreamDatasources {
        name
        }
        query
    }
  }
}
"""

with server.auth.sign_in(tableau_auth):
    custom_table_query_result = server.metadata.query(
        custom_table_query
    )

Según nuestros datos simulados, así es como se vería nuestro resultado:

{
  "data": {
    "customSQLTablesConnection": {
      "nodes": [
        {
          "id": "csql-1234",
          "name": "RegionalSales_CustomSQL",
          "downstreamDatasources": [
            {
              "name": "Regional Sales Trends (Custom SQL)"
            }
          ],
          "query": "SELECT r.region_name, SUM(s.sales_amount) AS total_sales FROM ecommerce.sales_data.Sales s JOIN ecommerce.sales_data.Regions r ON s.region_id = r.region_id GROUP BY r.region_name"
        },
        {
          "id": "csql-5678",
          "name": "ProfitabilityAnalysis_CustomSQL",
          "downstreamDatasources": [
            {
              "name": "Product Returns and Profitability"
            }
          ],
          "query": "SELECT p.product_category, SUM(s.profit) AS total_profit FROM ecommerce.sales_data.Sales s JOIN ecommerce.sales_data.Products p ON s.product_id = p.product_id GROUP BY p.product_category"
        },
        {
          "id": "csql-9101",
          "name": "CustomerSegmentation_CustomSQL",
          "downstreamDatasources": [
            {
              "name": "Customer Segmentation Analysis"
            }
          ],
          "query": "SELECT c.customer_id, c.location, COUNT(o.order_id) AS total_orders FROM ecommerce.sales_data.Customers c JOIN ecommerce.sales_data.Orders o ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.location"
        },
        {
          "id": "csql-3141",
          "name": "CustomerOrders_CustomSQL",
          "downstreamDatasources": [
            {
              "name": "Customer Orders DataSource"
            }
          ],
          "query": "SELECT o.order_id, o.customer_id, o.order_date, o.sales_amount FROM ecommerce.sales_data.Orders o WHERE o.order_status = 'Completed'"
        },
        {
          "id": "csql-3142",
          "name": "CustomerProfiles_CustomSQL",
          "downstreamDatasources": [
            {
              "name": "Customer Orders DataSource"
            }
          ],
          "query": "SELECT c.customer_id, c.customer_name, c.segment, c.location FROM ecommerce.sales_data.Customers c WHERE c.active_flag = 1"
        },
        {
          "id": "csql-3143",
          "name": "CustomerReturns_CustomSQL",
          "downstreamDatasources": [
            {
              "name": "Customer Orders DataSource"
            }
          ],
          "query": "SELECT r.return_id, r.order_id, r.return_reason FROM ecommerce.sales_data.Returns r"
        }
      ]
    }
  }
}

Al igual que antes, cuando estábamos creando la lista maestra de fuentes de datos, aquí también hemos anidado JSON para las fuentes de datos posteriores donde necesitaríamos extraer solo el «nombre». En la columna «Consulta», se descarga todo el SQL personalizado. Si usamos el patrón regex, podemos buscar fácilmente los nombres de la tabla utilizada en la consulta.

Sabemos que los nombres de la tabla siempre vienen después de una cláusula de unión y generalmente siguen el formato <database_name>.<schema>.<table_name>. El <database_name> es opcional y la mayoría de las veces no se usan. Hubo algunas consultas que encontré que usaban este formato y terminé solo obteniendo los nombres de la base de datos y el esquema, y ​​no el nombre completo de la tabla. Una vez que hemos extraído los nombres de las fuentes de datos y los nombres de las tablas, necesitamos fusionar las filas por fuente de datos, ya que puede haber múltiples consultas SQL personalizadas utilizadas en una sola fuente de datos.

### Convert the custom sql response into dataframe
col_names = custom_table_query_result['data']['customSQLTablesConnection']['nodes'][0].keys()
cs_df = pd.DataFrame(columns=col_names)

for i in custom_table_query_result['data']['customSQLTablesConnection']['nodes']:
    tmp_dt = {k:v for k,v in i.items()}

    cs_df = pd.concat([cs_df, pd.DataFrame.from_dict(tmp_dt, orient='index').T])

# Extract the data source name where the custom sql query was used
cs_df['data_source'] = cs_df.downstreamDatasources.apply(lambda x: x[0]['name'] if x and 'name' in x[0] else None)
cs_df.reset_index(inplace=True)
cs_df.drop(['index','downstreamDatasources'], axis=1,inplace=True)

### We need to extract the table names from the sql query. We know the table name comes after FROM or JOIN clause
# Note that the name of table can be of the format <data_warehouse>.<schema>.<table_name>
# Depending on the format of how table is called, you will have to modify the regex expression

def extract_tables(sql):
    # Regex to match database.schema.table or schema.table, avoid alias
    pattern = r'(?:FROM|JOIN)s+((?:[w+]|w+).(?:[w+]|w+)(?:.(?:[w+]|w+))?)b'
    matches = re.findall(pattern, sql, re.IGNORECASE)
    return list(set(matches))  # Unique table names

cs_df['customSQLTables'] = cs_df['query'].apply(extract_tables)
cs_df = cs_df[['data_source','customSQLTables']]

# We need to merge datasources as there can be multiple custom sqls used in the same data source
cs_df = cs_df.groupby('data_source', as_index=False).agg({
    'customSQLTables': lambda x: list(set(item for sublist in x for item in sublist))  # Flatten & make unique
})

print('There are ', cs_df.shape[0], 'datasources with custom sqls used in it')

Después de realizar todas las operaciones anteriores, así es como la estructura de cs_df se vería como:

Ejemplo de salida del código

Procesamiento de tablas regulares en fuentes de datos

Ahora necesitamos obtener la lista de todas las tablas regulares utilizadas en una fuente de datos que no forman parte de SQL personalizado. Hay dos formas de hacerlo. O use el publishedDatasources objeto y verificar upstreamTables o usar DatabaseTable y verificar upstreamDatasources. Pasaré el primer método porque quiero los resultados en un nivel de fuente de datos (básicamente, quiero que se esté listo un código para reutilizar cuando quiero verificar una fuente de datos específica con más detalle). Aquí nuevamente, en aras de la simplicidad, en lugar de ir a la paginación, estoy paseando a través de cada fuente de datos para asegurarme de tener todo. Obtenemos el upstreamTables Dentro del objeto de campo, eso debe limpiarse.

############### Get the data sources with the regular table names used in your site

### Its best to extract the tables information for every data source and then merge the results.
# Since we only get the table information nested under fields, in case there are hundreds of fields 
# used in a single data source, we will hit the response limits and will not be able to retrieve all the data.

data_source_list = master_df.name.tolist()

col_names = ['name', 'id', 'extractLastUpdateTime', 'fields']
ds_df = pd.DataFrame(columns=col_names)

with server.auth.sign_in(tableau_auth):
    for ds_name in data_source_list:
        query = """ {
            publishedDatasources (filter: { name: """"+ ds_name + """" }) {
            name
            id
            extractLastUpdateTime
            fields {
                name
                upstreamTables {
                    name
                }
            }
            }
        } """
        ds_name_result = server.metadata.query(
        query
        )
        for i in ds_name_result['data']['publishedDatasources']:
            tmp_dt = {k:v for k,v in i.items() if k != 'fields'}
            tmp_dt['fields'] = json.dumps(i['fields'])
        ds_df = pd.concat([ds_df, pd.DataFrame.from_dict(tmp_dt, orient='index').T])

ds_df.reset_index(inplace=True)

Así es como la estructura de ds_df se vería:

Ejemplo de salida del código

Podemos necesitar aplanar el fields Objeto y extraiga los nombres de campo, así como los nombres de la tabla. Dado que los nombres de la tabla se repitirán varias veces, tendríamos que deduplicarnos para mantener solo los únicos.

# Function to extract the values of fields and upstream tables in json lists
def extract_values(json_list, key):
    values = []
    for item in json_list:
        values.append(item[key])
    return values

ds_df["fields"] = ds_df["fields"].apply(ast.literal_eval)
ds_df['field_names'] = ds_df.apply(lambda x: extract_values(x['fields'],'name'), axis=1)
ds_df['upstreamTables'] = ds_df.apply(lambda x: extract_values(x['fields'],'upstreamTables'), axis=1)

# Function to extract the unique table names 
def extract_upstreamTable_values(table_list):
    values = set()a
    for inner_list in table_list:
        for item in inner_list:
            if 'name' in item:
                values.add(item['name'])
    return list(values)

ds_df['upstreamTables'] = ds_df.apply(lambda x: extract_upstreamTable_values(x['upstreamTables']), axis=1)
ds_df.drop(["index","fields"], axis=1, inplace=True)

Una vez que hacemos las operaciones anteriores, la estructura final de ds_df se vería algo así:

Ejemplo de salida del código

Tenemos todas las piezas y ahora solo tenemos que fusionarlas:

###### Join all the data together
master_data = pd.merge(master_df, ds_df, how="left", on=["name","id"])
master_data = pd.merge(master_data, cs_df, how="left", left_on="name", right_on="data_source")

# Save the results to analyse further
master_data.to_excel("Tableau Data Sources with Tables.xlsx", index=False)

Esta es nuestra final master_data:

Ejemplo de salida del código

Análisis de impacto a nivel de tabla

Digamos que hubo algunos cambios de esquema en la tabla de «ventas» y desea saber qué fuentes de datos se verán afectadas. Entonces simplemente puede escribir una función pequeña que verifica si una tabla está presente en cualquiera de las dos columnas –upstreamTableso customSQLTables como abajo.

def filter_rows_with_table(df, col1, col2, target_table):
    """
    Filters rows in df where target_table is part of any value in either col1 or col2 (supports partial match).
    Returns full rows (all columns retained).
    """
    return df[
        df.apply(
            lambda row: 
                (isinstance(row[col1], list) and any(target_table in item for item in row[col1])) or
                (isinstance(row[col2], list) and any(target_table in item for item in row[col2])),
            axis=1
        )
    ]
# As an example 
filter_rows_with_table(master_data, 'upstreamTables', 'customSQLTables', 'Sales')

A continuación se muestra la salida. Puede ver que este cambio afectará 3 fuentes de datos. También puede alertar a los propietarios de la fuente de datos Alice y Bob por adelantado sobre esto para que puedan comenzar a trabajar en una solución antes de que algo se rompa en los paneles de Tableau.

Ejemplo de salida del código

Puede consultar la versión completa del código en mi repositorio de GitHub aquí.

Este es solo uno de los posibles casos de uso de la API de metadatos de Tableau. También puede extraer los nombres de campo utilizados en consultas SQL personalizadas y agregar al conjunto de datos para obtener un análisis de impacto a nivel de campo. También se puede monitorear las fuentes de datos obsoletas con el extractLastUpdateTime Para ver si tienen algún problema o es necesario archivarse si ya no se usan. También podemos usar el dashboards OBJETO para obtener información a nivel de tablero.

Pensamientos finales

Si has llegado tan lejos, felicitaciones. Este es solo un caso de uso para automatizar la gestión de datos de Tableau. Es hora de reflexionar sobre su propio trabajo y pensar cuál de esas otras tareas podría automatizar para facilitar su vida. Espero que este mini proyecto haya servido como una experiencia de aprendizaje agradable para comprender el poder de la API de metadatos de Tableau. Si te gustó leer esto, también te gustará otra de mis publicaciones de blog sobre Tableau, en algunos de los desafíos que enfrenté al tratar con Big.

También consulte mi blog anterior donde exploré construir una aplicación interactiva con base de datos con Python, Streamlit y SQLite.


Antes de que te vayas …

Sígueme para que no te pierdas ninguna publicación nueva que escriba en el futuro; Encontrarás más de mis artículos en mi . También puedes conectarte conmigo en LinkedIn o Gorjeo!

Por automata