Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create config file if not exists #297

Merged
merged 1 commit into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}' \
Expand Down
58 changes: 54 additions & 4 deletions src/dbt_bouncer/config_file_validator.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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[
Expand Down Expand Up @@ -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.

Expand All @@ -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}."
Expand Down
4 changes: 3 additions & 1 deletion src/dbt_bouncer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
29 changes: 28 additions & 1 deletion tests/unit/test_config_file_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = [
Expand Down