def parse_lookml_file(lookml_file_name: str) -> dict: """Parse a LookML file into a dictionary with keys for each of its primary properties and a list of values.""" logger.info("Parsing data from LookML file {}".format(lookml_file_name)) with open(lookml_file_name, "r") as lookml_file_stream: lookml_data = lkml.load(lookml_file_stream) return lookml_data
def run_dbt_debug(self, *args, **kwargs) -> None: """Run `dbt debug` command to check if your dbt_project.yml and profiles.yml files are properly configured.""" logger.info( "Confirming proper dbt project setup, profile and warehouse access..." ) result = self._dbt_cli_runner(DBT_DEBUG, *args, **kwargs) logger.info(result)
def assemble_view( cls, view_name: str, sql_table_name: str = None, derived_table: str = None, dimensions: List[dict] = None, dimension_groups: List[dict] = None, measures: List[dict] = None, sets: List[dict] = None, parameters: List[dict] = None, label: str = None, required_access_grants: list = None, extends: str = None, extension_is_required: bool = False, include_suggestions: bool = True, ): assembled_view_dict = {"view": {"name": view_name}} logger.info("Creating LookML View: {}".format(view_name)) # Validate inputs if not sql_table_name and not derived_table and not extends: raise DbteaException( name="missing-lookml-view-properties", title="Missing Necessary LookML View Properties", detail="Created LookML Views must specify either a `sql_table_name`, `derived_table` or `extends` in order " "to properly specify the view source", ) # Add optional view options as needed if label: assembled_view_dict["view"]["label"] = label if extends: assembled_view_dict["view"]["extends"] = extends if extension_is_required: assembled_view_dict["view"]["extension"] = "required" if sql_table_name: assembled_view_dict["view"]["sql_table_name"] = sql_table_name if derived_table: assembled_view_dict["view"]["derived_table"] = derived_table if required_access_grants: assembled_view_dict["view"][ "required_access_grants" ] = required_access_grants if not include_suggestions: assembled_view_dict["view"]["suggestions"] = "no" # Add body of View if parameters: assembled_view_dict["view"]["parameters"] = parameters if dimensions: assembled_view_dict["view"]["dimensions"] = dimensions if dimension_groups: assembled_view_dict["view"]["dimension_groups"] = dimension_groups if measures: assembled_view_dict["view"]["measures"] = measures if sets: assembled_view_dict["view"]["sets"] = sets return lkml.dump(assembled_view_dict)
def write_data_to_file(self, replace_if_exists: bool = True) -> None: """""" if self._config_file_exists() and self._config_data_exists( ) and not replace_if_exists: logger.warning( "Dbtea config file already exists, ignoring write config data to file step" ) else: logger.info("Creating {} file at path: {}".format( self.file_name, self.config_dir)) utils.write_to_yaml_file(self.config_data, self.config_name_and_path)
def run_dbt_run_operation( self, macro_name: str, macro_args: dict = None, *args, **kwargs ) -> None: """Run `dbt run-operation` command to run dbt macros.""" logger.info("Executing dbt macro operation {}...".format(macro_name)) operation_with_macro = DBT_RUN_OPERATION.copy() operation_with_macro.append(macro_name) if macro_args: operation_with_macro.append(f"--args '{macro_args}'") result = self._dbt_cli_runner(operation_with_macro, *args, **kwargs) logger.info(result)
def write_looker_config(self): """""" logger.info("Writing Looker config file at path: {}".format( self.looker_config_path)) self.looker_config.add_section(self.looker_config_section) for key, value in self.config_data.get(self.dbt_project, {}).items(): if key.startswith("looker_sdk_"): self.looker_config.set(self.looker_config_section, str(key.replace("looker_sdk_", "")), str(value)) with open(self.looker_config_path, "w") as config_stream: self.looker_config.write(config_stream)
def timed_function(*args, **kwargs): start_time = timeit.default_timer() try: result = fn(*args, **kwargs) finally: elapsed_time = timeit.default_timer() - start_time elapsed_formatted = human_readable(elapsed_time) message_detail = get_detail(fn.__name__) logger.info( f"Completed {message_detail}operation in {elapsed_formatted}.\n" ) return result
def read_data_from_file(self, local_lookml_project_path: str) -> dict: """Parse a LookML file into a dictionary with keys for each of its primary properties and a list of values.""" logger.info( "Parsing data from local LookML file {}".format( self.lookml_file_name_and_path ) ) with open( utils.assemble_path( local_lookml_project_path, self.lookml_file_name_and_path ), "r", ) as lookml_file: return lkml.load(lookml_file)
def create_pull_request( organization_name: str, repository_name: str, git_token: str, head_branch: str, base_branch: str = "main", title: str = "dbtea updates", description: str = "dbtea metadata refresh", ): """Creates the pull request for the head_branch against the base_branch""" github_pulls_url = utils.assemble_path(GITHUB_API_URL, "repos", organization_name, repository_name, "pulls") headers = { "Authorization": "token {}".format(git_token), "Content-Type": "application/json", } payload = { "title": title, "body": description, "head": head_branch, "base": base_branch, } response = requests.post(github_pulls_url, headers=headers, data=json.dumps(payload)) if response.status_code >= 400: raise GitException( name="pull-request-create-fail", provider="github", title="Error Creating GitHub Pull Request via API", status=response.status_code, detail=response.json().get("errors"), response=response, ) logger.info("Created pull request for branch {} at URL: {}".format( head_branch, response.json().get("html_url")))
def fetch_dbt_project_directory(custom_project_directory: str = None) -> str: """Return path to the base of the closest dbt project by traversing from current working directory backwards in order to find a dbt_project.yml file. If an optional custom project path is specified (which should be a full path to the base project path of a dbt project), return that directory instead. """ project_directory = os.getcwd() root_path = os.path.abspath(os.sep) if custom_project_directory: custom_directory_project_file = assemble_path(custom_project_directory, DBT_PROJECT_FILE) if os.path.exists(custom_directory_project_file): return custom_project_directory else: raise DbteaException( name="invalid-custom-dbt-project-directory", title="No dbt project found at supplied custom directory", detail= "No dbt_project.yml file found at supplied custom project directory {}, confirm your " "custom project directory is valid".format( custom_project_directory), ) while project_directory != root_path: dbt_project_file = assemble_path(project_directory, DBT_PROJECT_FILE) if os.path.exists(dbt_project_file): logger.info("Running dbtea against dbt project at path: {}".format( project_directory)) return project_directory project_directory = os.path.dirname(project_directory) raise DbteaException( name="missing-dbt-project", title="No dbt project found", detail= "No dbt_project.yml file found in current or any direct parent paths. You need to run dbtea " "from within dbt project in order to use its tooling, or supply a custom project directory", )
def wrapper(*args, **kwargs): try: return function(*args, **kwargs) except DbteaException as error: logger.error( f"\n{error}\n\n" + "For support, please create an issue at https://github.com/spectacles-ci/spectacles/issues" + "\n") sys.exit(error.exit_code) except KeyboardInterrupt as error: logger.debug(error, exc_info=True) logger.info("Spectacles was manually interrupted.") sys.exit(1) except Exception as error: logger.debug(error, exc_info=True) logger.error( f'\nEncountered unexpected {error.__class__.__name__}: "{error}"\n' f"Full error traceback logged to file.\n\n" + "For support, please create an issue at https://github.com/spectacles-ci/spectacles/issues" + "\n") sys.exit(1)
def _dbt_cli_runner(self, input_command_as_list: list, *args, **kwargs): """Run dbt CLI command based on input options.""" arg_flags = list() kwarg_flags = list() if args: for flag in args: arg_flags.append(f"--{flag}") input_command_as_list.extend(arg_flags) if kwargs: for flag, flag_value in kwargs.items(): kwarg_flags.append(f"--{flag} '{flag_value}'") input_command_as_list.extend(kwarg_flags) input_command = " ".join(input_command_as_list) logger.info("Running dbt command: ".format(" ".join(input_command_as_list))) return utils.run_cli_command( input_command, working_directory=self.project_root, use_shell=True, output_as_text=True, capture_output=True, )
def run_dbt_deps(self, require_codegen: bool = False, *args, **kwargs) -> None: """Run `dbt deps` command to install dbt project dependencies; the `codegen` package must be included.""" project_packages_file = utils.assemble_path(self.project_root, "packages.yml") if require_codegen: if not utils.file_exists(project_packages_file): raise FileExistsError( "You must have a packages.yml file specified in your project" ) package_data = utils.parse_yaml_file(project_packages_file) package_list = [ entry.get("package") for entry in package_data.get("packages", {}) ] if "fishtown-analytics/codegen" not in package_list: raise ValueError( "You have not brought the codegen dbt package into your project! You must include the " "package 'fishtown-analytics/codegen' in your `packages.yml` file to codegen in bulk." ) logger.info("Fetching dbt project package dependencies...") result = self._dbt_cli_runner(DBT_DEPS, *args, **kwargs) logger.info(result)
def create_lookml_model( model_name: str, output_to: str = "stdout", connection: str = None, label: str = None, includes: list = None, explores: List[dict] = None, access_grants: List[dict] = None, tests: List[dict] = None, datagroups: List[dict] = None, map_layers: List[dict] = None, named_value_formats: List[dict] = None, fiscal_month_offset: int = None, persist_for: str = None, persist_with: str = None, week_start_day: str = None, case_sensitive: bool = True, output_directory: str = None, ) -> Optional[str]: """""" assembled_model_dict = dict() logger.info("Creating LookML Model: {}".format(model_name)) # Validate inputs if output_to not in OUTPUT_TO_OPTIONS: raise DbteaException( name="invalid-lookml-model-properties", title="Invalid LookML Model Properties", detail="You must choose a valid output_to option from the following: {}".format( OUTPUT_TO_OPTIONS ), ) if output_to == "file" and not output_directory: raise DbteaException( name="missing-output-directory", title="No Model Output Directory Specified", detail="You must include an output_directory param if outputting model to a file", ) # Add optional model options if connection: assembled_model_dict["connection"] = connection if label: assembled_model_dict["label"] = label if includes: assembled_model_dict["includes"] = includes if persist_for: assembled_model_dict["persist_for"] = persist_for if persist_with: assembled_model_dict["persist_with"] = persist_with if fiscal_month_offset: assembled_model_dict["fiscal_month_offset"] = fiscal_month_offset if week_start_day: assembled_model_dict["week_start_day"] = week_start_day if not case_sensitive: assembled_model_dict["case_sensitive"] = "no" # Add body of Model if datagroups: assembled_model_dict["datagroups"] = datagroups if access_grants: assembled_model_dict["access_grants"] = access_grants if explores: assembled_model_dict["explores"] = explores if named_value_formats: assembled_model_dict["named_value_formats"] = named_value_formats if map_layers: assembled_model_dict["map_layers"] = map_layers if tests: assembled_model_dict["tests"] = tests if output_to == "stdout": return lkml.dump(assembled_model_dict) else: model_file_name = utils.assemble_path( output_directory, model_name + ".model.lkml" ) with open(model_file_name, "w") as output_stream: output_stream.write(lkml.dump(assembled_model_dict))
def run_dbt_init(self, *args, **kwargs) -> None: """Run `dbt init` command to create a base dbt project.""" logger.info("Initialize a base dbt project...") result = self._dbt_cli_runner(DBT_INIT, *args, **kwargs) logger.info(result)
def assemble_model( cls, model_name: str, directory_path: str, connection: str = None, label: str = None, includes: list = None, explores: List[dict] = None, access_grants: List[dict] = None, tests: List[dict] = None, datagroups: List[dict] = None, map_layers: List[dict] = None, named_value_formats: List[dict] = None, fiscal_month_offset: int = None, persist_for: str = None, persist_with: str = None, week_start_day: str = None, case_sensitive: bool = True, ): """""" assembled_model_dict = dict() logger.info("Creating LookML Model: {}".format(model_name)) # Add optional model options if connection: assembled_model_dict["connection"] = connection if label: assembled_model_dict["label"] = label if includes: assembled_model_dict["includes"] = includes if persist_for: assembled_model_dict["persist_for"] = persist_for if persist_with: assembled_model_dict["persist_with"] = persist_with if fiscal_month_offset: assembled_model_dict["fiscal_month_offset"] = fiscal_month_offset if week_start_day: assembled_model_dict["week_start_day"] = week_start_day if not case_sensitive: assembled_model_dict["case_sensitive"] = "no" # Add body of Model if datagroups: assembled_model_dict["datagroups"] = datagroups if access_grants: assembled_model_dict["access_grants"] = access_grants if explores: assembled_model_dict["explores"] = explores if named_value_formats: assembled_model_dict["named_value_formats"] = named_value_formats if map_layers: assembled_model_dict["map_layers"] = map_layers if tests: assembled_model_dict["tests"] = tests return super().__init__( model_name, "model", directory_path=directory_path, lookml_data=assembled_model_dict, )
def run_dbt_parse(self, *args, **kwargs) -> None: """Run `dbt parse` command to provide information on performance.""" logger.info("Parsing dbt project for performance details...") result = self._dbt_cli_runner(DBT_PARSE, *args, **kwargs) logger.info(result)
def run_dbt_rpc(self, *args, **kwargs) -> None: """Run `dbt rpc` command to spin up an RPC server.""" logger.info("Starting dbt RPC server...") result = self._dbt_cli_runner(DBT_RPC, *args, **kwargs) logger.info(result)
def run_dbt_run(self, *args, **kwargs) -> None: """Run `dbt run` command to run dbt models.""" logger.info("Running dbt models...") result = self._dbt_cli_runner(DBT_RUN, *args, **kwargs) logger.info(result)
def run_dbt_source_snapshot_freshness(self, *args, **kwargs) -> None: """Run `dbt source snapshot-freshness` command to get freshness of data sources.""" logger.info("Checking freshness of data source tables...") result = self._dbt_cli_runner(DBT_SOURCE_SNAPSHOT_FRESHNESS, *args, **kwargs) logger.info(result)
def run_dbt_seed(self, *args, **kwargs) -> None: """Run `dbt seed` command to upload seed data.""" logger.info("Uploading dbt seed data files to data warehouse...") result = self._dbt_cli_runner(DBT_SEED, *args, **kwargs) logger.info(result)
def run_dbt_docs_serve(self, *args, **kwargs) -> None: """Run `dbt docs serve` command to view dbt documentation.""" logger.info("Serving dbt documentation site...") result = self._dbt_cli_runner(DBT_DOCS_SERVE, *args, **kwargs) logger.info(result)
def run_dbt_test(self, *args, **kwargs) -> None: """Run `dbt test` command to run dbt models.""" logger.info("Running dbt tests to validate models...") result = self._dbt_cli_runner(DBT_TEST, *args, **kwargs) logger.info(result)
def run_dbt_list(self, *args, **kwargs) -> None: """Run `dbt list` (ls) command to list all resources within the dbt project.""" logger.info("Listing dbt project resources...") result = self._dbt_cli_runner(DBT_LIST, *args, **kwargs) logger.info(result)
def run_dbt_clean(self, *args, **kwargs) -> None: """Run `dbt clean` command to remove clean target folders (usually dbt_modules, target) from dbt project.""" logger.info("Removing dbt clean target folders from dbt project...") result = self._dbt_cli_runner(DBT_CLEAN, *args, **kwargs) logger.info(result)
def run_dbt_snapshot(self, *args, **kwargs) -> None: """Run `dbt snapshot` command to execute dbt snapshots.""" logger.info("Running dbt snapshots...") result = self._dbt_cli_runner(DBT_SNAPSHOT, *args, **kwargs) logger.info(result)
def run_dbt_compile(self, *args, **kwargs) -> None: """Run `dbt compile` command to compile dbt models.""" logger.info("Compiling dbt models...") result = self._dbt_cli_runner(DBT_COMPILE, *args, **kwargs) logger.info(result)
def run_dbt_docs_generate(self, *args, **kwargs) -> None: """Run `dbt docs generate` command to generate dbt documentation artifacts.""" logger.info("Generating dbt documentation artifacts...") result = self._dbt_cli_runner(DBT_DOCS_GENERATE, *args, **kwargs) logger.info(result)