Ya escribí sobre la función de documentación en dbt y cómo ayuda a crear documentación consistente y precisa en todo el proyecto dbt (ver este). En resumen, puede almacenar la descripción de las columnas más comunes/importantes utilizadas en los modelos de datos de su proyecto agregándolas en el docs.md archivos, que se encuentran en la carpeta docs de su proyecto dbt.
Un ejemplo muy sencillo de pedidos.md archivo que contiene la descripción de los nombres de columnas más comunes relacionados con el cliente:
# Fields description## order_id
{% docs orders__order_id %}
Unique alphanumeric identifier of the order, used to join all order dimension tables
{% enddocs %}
## order_country
{% docs orders__order_country %}
Country where the order was placed. Format is country ISO 3166 code.
{% enddocs %}
## order_value
{% docs orders__value %}
Total value of the order in local currency.
{% enddocs %}
## order_date
{% docs orders__date %}
Date of the order in local timezone
{% enddocs %}
Y su uso en el .yml archivo de un modelo:
columns:
- name: order_id
description: '{{ doc("orders__order_id") }}'
Cuando se generan los documentos dbt, la descripción de order_id será siempre la misma, siempre que el doc La función se utiliza en el archivo yml del modelo. El beneficio de tener esta documentación centralizada es claro e innegable.
El desafío
Sin embargo, especialmente con proyectos grandes y cambios frecuentes (nuevos modelos o cambios en los existentes), es probable que los contribuyentes del repositorio se olviden de usar el doc función, o no son conscientes de que se ha agregado una columna específica a la documentos carpeta. Esto tiene dos consecuencias:
- alguien debe detectar esto durante la revisión de relaciones públicas y solicitar un cambio, suponiendo que haya al menos un revisor que sepa de memoria todas las columnas documentadas o que siempre revise la carpeta de documentos manualmente.
- Si bien es fácil pasar desapercibido y depende de individuos, esta configuración anula el propósito de tener una documentación centralizada.
la solución
La respuesta simple a este problema es una verificación de CI (integración continua), que combina un flujo de trabajo de GitHub con un script de Python. Esta verificación falla si:
- Los cambios en el PR están afectando a un archivo .yml que contiene un nombre de columna presente en los documentos, pero el doc la función no se utiliza para esa columna
- Los cambios en el PR están afectando un archivo .yml que contiene un nombre de columna presente en los documentos, pero esa columna no tiene ninguna descripción.
Echemos un vistazo más de cerca al código y los archivos necesarios para ejecutar esta verificación y a un par de ejemplos. Como se mencionó anteriormente, hay dos cosas a considerar: una (1) Archivo .yml para el flujo de trabajo y un (2) archivo Python para la verificación de validación real.
(1) Así es como el documentos_validación parece el archivo. Se coloca en el github/flujos de trabajo carpeta.
name: Validate Documentationon:
pull_request:
types: [opened, synchronize, reopened]
jobs:
validate_docs:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install PyYAML
run: pip install pyyaml
- name: Run validation script
run: python validate_docs.py
El flujo de trabajo se ejecutará cada vez que se abra o se vuelva a abrir una solicitud de extracción, y cada vez que se envíe una nueva confirmación a la rama remota. Luego, hay básicamente 3 pasos: recuperar los archivos del repositorio para la solicitud de extracción actual, instalar las dependencias y ejecutar el script de validación.
(2). Entonces el validar_docs.py script, ubicado en la carpeta raíz de su repositorio de proyectos dbt, que se ve así
import os
import sys
import yaml
import re
from glob import glob
from pathlib import Path
import subprocessdef get_changed_files():
diff_command = ['git', 'diff', '--name-only', 'origin/main...']
result = subprocess.run(diff_command, capture_output=True, text=True)
changed_files = result.stdout.strip().split('\n')
return changed_files
def extract_doc_names():
doc_names = set()
md_files = glob('docs/**/*.md', recursive=True)
doc_pattern = re.compile(r'\{%\s*docs\s+([^\s%]+)\s*%}')
for md_file in md_files:
with open(md_file, 'r') as f:
content = f.read()
matches = doc_pattern.findall(content)
doc_names.update(matches)
print(f"Extracted doc names: {doc_names}")
return doc_names
def parse_yaml_file(yaml_path):
with open(yaml_path, 'r') as f:
try:
return list(yaml.safe_load_all(f))
except yaml.YAMLError as exc:
print(f"Error parsing YAML file {yaml_path}: {exc}")
return []
def validate_columns(columns, doc_names, errors, model_name):
for column in columns:
column_name = column.get('name')
description = column.get('description', '')
print(f"Validating column '{column_name}' in model '{model_name}'")
print(f"Description: '{description}'")
doc_usage = re.findall(r'\{\{\s*doc\(["\']([^"\']+)["\']\)\s*\}\}', description)
print(f"Doc usage found: {doc_usage}")
if doc_usage:
for doc_name in doc_usage:
if doc_name not in doc_names:
errors.append(
f"Column '{column_name}' in model '{model_name}' references undefined doc '{doc_name}'."
)
else:
matching_docs = [dn for dn in doc_names if dn.endswith(f"__{column_name}")]
if matching_docs:
suggested_doc = matching_docs[0]
errors.append(
f"Column '{column_name}' in model '{model_name}' should use '{{{{ doc(\"{suggested_doc}\") }}}}' in its description."
)
else:
print(f"No matching doc found for column '{column_name}'")
def main():
changed_files = get_changed_files()
yaml_files = [f for f in changed_files if f.endswith('.yml') or f.endswith('.yaml')]
doc_names = extract_doc_names()
errors = []
for yaml_file in yaml_files:
if not os.path.exists(yaml_file):
continue
yaml_content = parse_yaml_file(yaml_file)
for item in yaml_content:
if not isinstance(item, dict):
continue
models = item.get('models') or item.get('sources')
if not models:
continue
for model in models:
model_name = model.get('name')
columns = model.get('columns', [])
validate_columns(columns, doc_names, errors, model_name)
if errors:
print("Documentation validation failed with the following errors:")
for error in errors:
print(f"- {error}")
sys.exit(1)
else:
print("All documentation validations passed.")
if __name__ == "__main__":
main()
Resumamos los pasos del script:
- enumera todos los archivos que se han modificado en la solicitud de extracción en comparación con la rama de origen.
- revisa todas las rebajas (.Maryland) archivos dentro del documentos carpeta (incluidos los subdirectorios) y busca patrones de bloques de documentación especiales utilizando una expresión regular. Cada vez que encuentra dicho patrón, extrae la parte doc_name y la agrega a un conjunto de nombres de documentos.
- para cada archivo .yml modificado, el script se abre y lo analiza usando yaml.safe_load_all. Esto convierte el contenido .yml en diccionarios (o listas) de Python para facilitar el análisis.
- columnas_validar: para cada columna definida en los archivos .yml, verifica el campo de descripción para ver si incluye un {{ doc() }} referencia. Si se encuentran referencias, verifica que el nombre del documento al que se hace referencia realmente existe en el conjunto de nombres de documentos extraídos anteriormente. Si no, informa un error. Si no se encuentran referencias de documentos, intenta ver si hay un bloque de documentos que coincida con el nombre de esta columna. Tenga en cuenta que aquí estamos usando una convención de nomenclatura como doc_block__nombre_columna. Si existe dicho bloque, sugiere que la descripción de la columna debe hacer referencia a este documento.
Cualquier problema (referencias de documentos faltantes, documentos referenciados inexistentes) se registra como error.
Ejemplos
Ahora, echemos un vistazo al CI en acción. Dado que pedidos.md archivo compartido al principio del artículo, ahora enviamos a control remoto esta confirmación que contiene el ref_country_orders.yml archivo:
version: 2models:
- name: ref_country_orders
description: >
This model filters orders from the staging orders table to include only those with an order date on or after January 1, 2020.
It includes information such as the order ID, order country, order value, and order date.
columns:
- name: order_id
description: '{{ doc("orders__order_id") }}'
- name: order_country
description: The country where the order was placed.
- name: order_value
description: The value of the order.
- name: order_address
description: The address where the order was placed.
- name: order_date
La IC ha fracasado. Al hacer clic en Detalles nos llevará al registro del CI, donde vemos esto:
Validating column 'order_id' in model 'ref_country_orders'
Description: '{{ doc("orders__order_id") }}'
Doc usage found: ['orders__order_id']
Validating column 'order_country' in model 'ref_country_orders'
Description: 'The country where the order was placed.'
Doc usage found: []
Validating column 'order_value' in model 'ref_country_orders'
Description: 'The value of the order.'
Doc usage found: []
Validating column 'order_address' in model 'ref_country_orders'
Description: 'The address where the order was placed.'
Doc usage found: []
No matching doc found for column 'order_address'
Validating column 'order_date' in model 'ref_country_orders'
Description: ''
Doc usage found: []
Analicemos el registro:
– para el id_pedido columna encontró el uso del documento en su descripción.
– el dirección_pedido La columna no se encuentra en el archivo docs, por lo que devuelve un No se encontró ningún documento coincidente para la columna ‘order_address’
– para el valor_pedido y orden_paíssabe que están enumerados en los documentos, pero el uso del documento está vacío. Lo mismo para el fecha_pedidoy tenga en cuenta que para este ni siquiera agregamos una línea descriptiva
Todo bien hasta ahora. Pero sigamos mirando el registro:
Documentation validation failed with the following errors:
- Column 'order_country' in model 'ref_country_orders' should use '{{ doc("orders__order_country") }}' in its description.
- Column 'order_value' in model 'ref_country_orders' should use '{{ doc("orders__order_value") }}' in its description.
- Column 'order_date' in model 'ref_country_orders' should use '{{ doc("orders__order_date") }}' in its description.
Error: Process completed with exit code 1.
Desde orden_país, valor_pedido, fecha_pedido están en el archivo de documentos, pero la función de documento no se utiliza, el CI genera un error. Y sugiere el valor real a agregar en la descripción, lo que hace que sea extremadamente fácil para el autor de relaciones públicas copiar y pegar el valor de descripción correcto del registro de CI y agregarlo al archivo .yml archivo.
Después de implementar los nuevos cambios, la verificación de CI fue exitosa y el registro ahora se ve así:
Validating column 'order_id' in model 'ref_country_orders'
Description: '{{ doc("orders__order_id") }}'
Doc usage found: ['orders__order_id']
Validating column 'order_country' in model 'ref_country_orders'
Description: '{{ doc("orders__order_country") }}'
Doc usage found: ['orders__order_country']
Validating column 'order_value' in model 'ref_country_orders'
Description: '{{ doc("orders__order_value") }}'
Doc usage found: ['orders__order_value']
Validating column 'order_address' in model 'ref_country_orders'
Description: 'The address where the order was placed.'
Doc usage found: []
No matching doc found for column 'order_address'
Validating column 'order_date' in model 'ref_country_orders'
Description: '{{ doc("orders__order_date") }}'
Doc usage found: ['orders__order_date']
All documentation validations passed.
Para el dirección_pedido columna, el registro muestra que no se encontró ningún documento coincidente. Sin embargo, eso está bien y no hace que el CI falle, ya que agregar esa columna al archivo de documentos no es nuestra intención para esta demostración. Mientras tanto, el resto de las columnas se enumeran en los documentos y utilizan correctamente el {{ doc() }} función