Source code for operators.dbt.dbt_docs
# dbt_test.py
from airflow.utils.decorators import apply_defaults
from airflow.utils.context import Context
from operators.dbt.dbt_base import BaseDBTOperator
from airflow.utils.decorators import apply_defaults
from airflow.hooks.base import BaseHook
from config import Config
import os
from typing import List, Optional, Dict, Any
[docs]
class DBTDocsOperator(BaseDBTOperator):
"""
Airflow operator to generate dbt docs, package them, and upload to S3.
:param select: List of models to include in the docs generation.
:param exclude: List of models to exclude from the docs generation.
:param bucket: S3 bucket to upload the generated docs.
:param s3_prefix: S3 prefix for the generated docs.
:param args: Additional arguments for the BaseDBTOperator.
:param kwargs: Additional keyword arguments for the BaseDBTOperator.
"""
ui_color = "#FFB433"
ui_fgcolor = "#000000"
custom_operator_name = "DBT Docs Operator"
@apply_defaults
def __init__(
self,
select: Optional[List[str]] = None,
exclude: Optional[List[str]] = None,
bucket: str = Config.DATAHUB_S3_BUCKET,
s3_prefix: str = "{env}/data_docs/dag={dag_id}/docs/{version}",
*args,
**kwargs,
) -> None:
"""
:param select: List of models to include in the docs generation.
:param exclude: List of models to exclude from the docs generation.
:param bucket: S3 bucket to upload the generated docs.
:param s3_prefix: S3 prefix for the generated docs.
:param args: Additional arguments for the BaseDBTOperator.
:param kwargs: Additional keyword arguments for the BaseDBTOperator.
"""
super().__init__(*args, **kwargs)
self.bucket = bucket
self.s3_prefix = s3_prefix
self.select = select
self.exclude = exclude
self.proxy_lambda = "https://5w3irbfxp5.execute-api.us-east-1.amazonaws.com/deploy/{proxy}/static_index.html"
[docs]
def execute(self, context: Dict[str, Any]) -> None:
"""Execute dbt docs generate and upload to S3."""
super().execute(context)
dbt_compile_cmd = " ".join(self.build_command("dbt compile"))
dbt_build_cmd = " ".join(self.build_command("dbt build"))
dbt_docs_cmd = " ".join(self.build_command("dbt docs generate --static"))
try:
self.log.info(f"Running: {dbt_compile_cmd}")
self.run_ssh_command(dbt_compile_cmd)
self.log.info(f"Running: {dbt_build_cmd}")
self.run_ssh_command(dbt_build_cmd)
self.run_ssh_command(f"cp {self.target_path}/run_results.json {self.target_path}/run_results_build.json")
self.log.info(f"Running: {dbt_docs_cmd}")
self.run_ssh_command(dbt_docs_cmd)
self.log.info("dbt docs generation completed successfully.")
self.red_datahub_recipe(context)
except Exception as e:
self.log.error(f"dbt docs generation failed! {e}")
raise
s3_key = self._get_s3_key()
self._upload_to_s3(self.target_path, s3_key)
# Generate and store public URL
public_url = self._generate_public_url(s3_key)
self.xcom_push(context, key="dbt_docs_url", value=public_url)
def _get_s3_key(self) -> str:
"""Generate S3 key for upload."""
return self.s3_prefix.format(
env=self.target, dag_id=self.dag_id, version=self.job_id
)
def _upload_to_s3(self, docs_path: str, s3_key: str) -> None:
"""Upload packaged docs to S3."""
self.run_ssh_command(
f"ls {docs_path} ; aws s3 sync {docs_path} s3://{self.bucket}/{s3_key}"
)
def _generate_public_url(self, s3_key: str) -> str:
"""Generate public URL using GeneratePresignedUrl Lambda."""
base_url = self.proxy_lambda.format(proxy=s3_key)
print(f"Generated public URL: {base_url}")
return base_url
[docs]
def replace_dbt_paths(self, obj, dbt_project_root):
if isinstance(obj, dict):
return {
k: self.replace_dbt_paths(v, dbt_project_root) for k, v in obj.items()
}
elif isinstance(obj, list):
return [self.replace_dbt_paths(item, dbt_project_root) for item in obj]
elif isinstance(obj, str):
return obj.replace("{DBT_PROJECT_ROOT}", dbt_project_root)
return obj
[docs]
def red_datahub_recipe(self, context: Context) -> Dict[str, Any]:
"""Return datahub recipe for the operator."""
import yaml
import tempfile
filename = "dbt_recipe.dhub.yaml"
recipe_path = f"{self.project_directory}/{filename}"
dag_name = context["dag"].dag_id
if os.path.exists(recipe_path):
with open(recipe_path, "r") as file:
recipe = yaml.safe_load(file)
else:
recipe = {}
# modify it
recipe = self.replace_dbt_paths(recipe, self.target_path)
print(f"Recipe: {recipe}")
if 'source' in recipe:
recipe["source"]['config']['env'] = "DEV"
# recipe['meta_mapping'] = {
# "dbt_project_root": self.project_directory,
# "dag_name": dag_name,
# "target_platform": self.target_platform,
# }
# write it back
recipe_temp_path= f"{self.target_path}/{filename}"
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as temp_file:
yaml.dump(recipe, temp_file)
temp_path = temp_file.name
ssh_target = f"{self.dbt_ssh_user}@{self.dbt_ssh_host}"
scp_command =[
'scp', temp_path, f'{ssh_target}:{recipe_temp_path}'
]
self._run_bash_command(' '.join(scp_command))
self.log.info(
f"Recipe written to {recipe_temp_path}"
)
self.run_ssh_command(
f"datahub ingest run -c {recipe_temp_path}"
)
self.run_ssh_command(f"datahub ingest list-runs")