diff --git a/action.yml b/action.yml index bf64de9a..72bc61b0 100644 --- a/action.yml +++ b/action.yml @@ -49,6 +49,7 @@ runs: shell: bash run: > docker run --rm \ + --env CREATE_DBT_BOUNCER_CONFIG_FILE=false \ --env GITHUB_REF='${{ github.ref }}' \ --env GITHUB_REPOSITORY='${{ github.repository }}' \ --env GITHUB_RUN_ID='${{ github.run_id }}' \ diff --git a/src/dbt_bouncer/config_file_validator.py b/src/dbt_bouncer/config_file_validator.py index 68661068..54106c39 100644 --- a/src/dbt_bouncer/config_file_validator.py +++ b/src/dbt_bouncer/config_file_validator.py @@ -1,8 +1,10 @@ import logging +import os import re from pathlib import Path, PurePath -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Mapping +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Mapping, Optional +import click import toml from pydantic import ValidationError @@ -13,6 +15,23 @@ DbtBouncerConfAllCategories as DbtBouncerConf, ) +DEFAULT_DBT_BOUNCER_CONFIG = """manifest_checks: + - name: check_model_directories + include: ^models + permitted_sub_directories: + - intermediate + - marts + - staging + - name: check_model_names + include: ^models/staging + model_name_pattern: ^stg_ +catalog_checks: + - name: check_columns_are_documented_in_public_models +run_results_checks: + - name: check_run_results_max_execution_time + max_execution_time_seconds: 60 +""" + def conf_cls_factory( check_categories: List[ @@ -120,9 +139,16 @@ def get_config_file_path( return pyproject_toml_dir / "pyproject.toml" -def load_config_file_contents(config_file_path: PurePath) -> Mapping[str, Any]: +def load_config_file_contents( + config_file_path: PurePath, + allow_default_config_file_creation: Optional[bool] = None, +) -> Mapping[str, Any]: """Load the contents of the config file. + Args: + config_file_path: Path to the config file. + allow_default_config_file_creation: Whether to allow the creation of a default config file if one does not exist. Used to allow pytesting of this function. + Returns: Mapping[str, Any]: Config for dbt-bouncer. @@ -137,9 +163,33 @@ def load_config_file_contents(config_file_path: PurePath) -> Mapping[str, Any]: if "dbt-bouncer" in toml_cfg["tool"]: return next(v for k, v in toml_cfg["tool"].items() if k == "dbt-bouncer") else: - raise RuntimeError( - "Please ensure your pyproject.toml file is correctly configured to work with `dbt-bouncer`. Alternatively, you can pass the path to your config file via the `--config-file` flag.", + logging.warning( + "Cannot find a `dbt-bouncer.yml` file or a `dbt-bouncer` section found in pyproject.toml." ) + if ( + allow_default_config_file_creation is True + and os.getenv("CREATE_DBT_BOUNCER_CONFIG_FILE") != "false" + and ( + os.getenv("CREATE_DBT_BOUNCER_CONFIG_FILE") == "true" + or click.confirm( + "Do you want `dbt-bouncer` to create a `dbt-bouncer.yml` file in the current directory?" + ) + ) + ): + created_config_file = Path.cwd().joinpath("dbt-bouncer.yml") + created_config_file.touch() + logging.info( + "A `dbt-bouncer.yml` file has been created in the current directory with default settings." + ) + with Path.open(created_config_file, "w") as f: + f.write(DEFAULT_DBT_BOUNCER_CONFIG) + + return load_config_from_yaml(created_config_file) + + else: + raise RuntimeError( + "No configuration for `dbt-bouncer` could be found. You can pass the path to your config file via the `--config-file` flag. Alternatively, your pyproject.toml file can be configured to work with `dbt-bouncer`.", + ) else: raise RuntimeError( f"Config file must be either a `pyproject.toml`, `.yaml` or `.yml` file. Got {config_file_path.suffix}." diff --git a/src/dbt_bouncer/main.py b/src/dbt_bouncer/main.py index af1f7a31..1c3b09a8 100644 --- a/src/dbt_bouncer/main.py +++ b/src/dbt_bouncer/main.py @@ -68,7 +68,9 @@ def cli( .get_parameter_source("config_file") .name, # type: ignore[union-attr] ) - config_file_contents = load_config_file_contents(config_file_path) + config_file_contents = load_config_file_contents( + config_file_path, allow_default_config_file_creation=True + ) # Handle `severity` at the global level if config_file_contents.get("severity"): diff --git a/tests/unit/test_config_file_validator.py b/tests/unit/test_config_file_validator.py index e8ca3d84..68dda26b 100644 --- a/tests/unit/test_config_file_validator.py +++ b/tests/unit/test_config_file_validator.py @@ -93,6 +93,30 @@ def test_get_file_config_path_pyproject_toml_recursive(monkeypatch, tmp_path): assert config_file_path == pyproject_file +def test_load_config_file_contents_create_default_config_file( + monkeypatch, + tmp_path, +): + monkeypatch.chdir(tmp_path) + + pyproject_file = tmp_path / "pyproject.toml" + config = {"tool": {"dbt-bouncer-misspelled": PYPROJECT_TOML_SAMPLE_CONFIG}} + with Path.open(pyproject_file, "w") as f: + toml.dump(config, f) + + with pytest.MonkeyPatch.context() as mp: + mp.setenv("CREATE_DBT_BOUNCER_CONFIG_FILE", "true") + + contents = load_config_file_contents( + config_file_path=pyproject_file, allow_default_config_file_creation=True + ) + assert list(contents.keys()) == [ + "manifest_checks", + "catalog_checks", + "run_results_checks", + ] + + def test_load_config_file_contents_pyproject_toml_no_bouncer_section( monkeypatch, tmp_path, @@ -105,7 +129,10 @@ def test_load_config_file_contents_pyproject_toml_no_bouncer_section( toml.dump(config, f) with pytest.raises(RuntimeError): - config = load_config_file_contents(config_file_path=tmp_path / "pyproject.toml") + config = load_config_file_contents( + config_file_path=tmp_path / "pyproject.toml", + allow_default_config_file_creation=False, + ) invalid_confs = [