altimate-datapilot-cli 0.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- altimate_datapilot_cli-0.0.8.dist-info/AUTHORS.rst +5 -0
- altimate_datapilot_cli-0.0.8.dist-info/LICENSE +9 -0
- altimate_datapilot_cli-0.0.8.dist-info/METADATA +102 -0
- altimate_datapilot_cli-0.0.8.dist-info/RECORD +139 -0
- altimate_datapilot_cli-0.0.8.dist-info/WHEEL +5 -0
- altimate_datapilot_cli-0.0.8.dist-info/entry_points.txt +4 -0
- altimate_datapilot_cli-0.0.8.dist-info/top_level.txt +1 -0
- datapilot/__init__.py +1 -0
- datapilot/__main__.py +14 -0
- datapilot/cli/__init__.py +0 -0
- datapilot/cli/main.py +11 -0
- datapilot/clients/__init__.py +0 -0
- datapilot/clients/altimate/__init__.py +0 -0
- datapilot/clients/altimate/client.py +85 -0
- datapilot/clients/altimate/utils.py +75 -0
- datapilot/config/__init__.py +0 -0
- datapilot/config/config.py +16 -0
- datapilot/config/utils.py +32 -0
- datapilot/core/__init__.py +0 -0
- datapilot/core/insights/__init__.py +2 -0
- datapilot/core/insights/base/__init__.py +0 -0
- datapilot/core/insights/base/insight.py +34 -0
- datapilot/core/insights/report.py +16 -0
- datapilot/core/insights/schema.py +24 -0
- datapilot/core/insights/sql/__init__.py +0 -0
- datapilot/core/insights/sql/base/__init__.py +0 -0
- datapilot/core/insights/sql/base/insight.py +18 -0
- datapilot/core/insights/sql/runtime/__init__.py +0 -0
- datapilot/core/insights/sql/static/__init__.py +0 -0
- datapilot/core/insights/utils.py +20 -0
- datapilot/core/platforms/__init__.py +0 -0
- datapilot/core/platforms/dbt/__init__.py +0 -0
- datapilot/core/platforms/dbt/cli/__init__.py +0 -0
- datapilot/core/platforms/dbt/cli/cli.py +112 -0
- datapilot/core/platforms/dbt/constants.py +34 -0
- datapilot/core/platforms/dbt/exceptions.py +6 -0
- datapilot/core/platforms/dbt/executor.py +157 -0
- datapilot/core/platforms/dbt/factory.py +22 -0
- datapilot/core/platforms/dbt/formatting.py +45 -0
- datapilot/core/platforms/dbt/hooks/__init__.py +0 -0
- datapilot/core/platforms/dbt/hooks/executor_hook.py +86 -0
- datapilot/core/platforms/dbt/insights/__init__.py +115 -0
- datapilot/core/platforms/dbt/insights/base.py +133 -0
- datapilot/core/platforms/dbt/insights/checks/__init__.py +0 -0
- datapilot/core/platforms/dbt/insights/checks/base.py +26 -0
- datapilot/core/platforms/dbt/insights/checks/check_column_desc_are_same.py +105 -0
- datapilot/core/platforms/dbt/insights/checks/check_column_name_contract.py +154 -0
- datapilot/core/platforms/dbt/insights/checks/check_macro_args_have_desc.py +75 -0
- datapilot/core/platforms/dbt/insights/checks/check_macro_has_desc.py +63 -0
- datapilot/core/platforms/dbt/insights/checks/check_model_has_all_columns.py +96 -0
- datapilot/core/platforms/dbt/insights/checks/check_model_has_labels_keys.py +112 -0
- datapilot/core/platforms/dbt/insights/checks/check_model_has_meta_keys.py +108 -0
- datapilot/core/platforms/dbt/insights/checks/check_model_has_properties_file.py +64 -0
- datapilot/core/platforms/dbt/insights/checks/check_model_has_tests_by_group.py +118 -0
- datapilot/core/platforms/dbt/insights/checks/check_model_has_tests_by_name.py +114 -0
- datapilot/core/platforms/dbt/insights/checks/check_model_has_tests_by_type.py +119 -0
- datapilot/core/platforms/dbt/insights/checks/check_model_materialization_by_childs.py +129 -0
- datapilot/core/platforms/dbt/insights/checks/check_model_name_contract.py +132 -0
- datapilot/core/platforms/dbt/insights/checks/check_model_parents_and_childs.py +135 -0
- datapilot/core/platforms/dbt/insights/checks/check_model_parents_database.py +109 -0
- datapilot/core/platforms/dbt/insights/checks/check_model_parents_schema.py +109 -0
- datapilot/core/platforms/dbt/insights/checks/check_model_tags.py +87 -0
- datapilot/core/platforms/dbt/insights/checks/check_source_childs.py +97 -0
- datapilot/core/platforms/dbt/insights/checks/check_source_columns_have_desc.py +96 -0
- datapilot/core/platforms/dbt/insights/checks/check_source_has_all_columns.py +103 -0
- datapilot/core/platforms/dbt/insights/checks/check_source_has_freshness.py +94 -0
- datapilot/core/platforms/dbt/insights/checks/check_source_has_labels_keys.py +110 -0
- datapilot/core/platforms/dbt/insights/checks/check_source_has_loader.py +62 -0
- datapilot/core/platforms/dbt/insights/checks/check_source_has_meta_keys.py +117 -0
- datapilot/core/platforms/dbt/insights/checks/check_source_has_tests.py +82 -0
- datapilot/core/platforms/dbt/insights/checks/check_source_has_tests_by_group.py +117 -0
- datapilot/core/platforms/dbt/insights/checks/check_source_has_tests_by_name.py +113 -0
- datapilot/core/platforms/dbt/insights/checks/check_source_has_tests_by_type.py +119 -0
- datapilot/core/platforms/dbt/insights/checks/check_source_table_has_description.py +62 -0
- datapilot/core/platforms/dbt/insights/checks/check_source_tags.py +76 -0
- datapilot/core/platforms/dbt/insights/dbt_test/__init__.py +0 -0
- datapilot/core/platforms/dbt/insights/dbt_test/base.py +23 -0
- datapilot/core/platforms/dbt/insights/dbt_test/missing_primary_key_tests.py +130 -0
- datapilot/core/platforms/dbt/insights/dbt_test/test_coverage.py +118 -0
- datapilot/core/platforms/dbt/insights/governance/__init__.py +0 -0
- datapilot/core/platforms/dbt/insights/governance/base.py +23 -0
- datapilot/core/platforms/dbt/insights/governance/documentation_on_stale_columns.py +130 -0
- datapilot/core/platforms/dbt/insights/governance/exposures_dependent_on_private_models.py +90 -0
- datapilot/core/platforms/dbt/insights/governance/public_models_without_contracts.py +89 -0
- datapilot/core/platforms/dbt/insights/governance/undocumented_columns.py +148 -0
- datapilot/core/platforms/dbt/insights/governance/undocumented_public_models.py +110 -0
- datapilot/core/platforms/dbt/insights/modelling/README.md +15 -0
- datapilot/core/platforms/dbt/insights/modelling/__init__.py +0 -0
- datapilot/core/platforms/dbt/insights/modelling/base.py +31 -0
- datapilot/core/platforms/dbt/insights/modelling/direct_join_to_source.py +125 -0
- datapilot/core/platforms/dbt/insights/modelling/downstream_models_dependent_on_source.py +113 -0
- datapilot/core/platforms/dbt/insights/modelling/duplicate_sources.py +85 -0
- datapilot/core/platforms/dbt/insights/modelling/hard_coded_references.py +80 -0
- datapilot/core/platforms/dbt/insights/modelling/joining_of_upstream_concepts.py +79 -0
- datapilot/core/platforms/dbt/insights/modelling/model_fanout.py +126 -0
- datapilot/core/platforms/dbt/insights/modelling/multiple_sources_joined.py +83 -0
- datapilot/core/platforms/dbt/insights/modelling/root_model.py +82 -0
- datapilot/core/platforms/dbt/insights/modelling/source_fanout.py +102 -0
- datapilot/core/platforms/dbt/insights/modelling/staging_model_dependent_on_downstream_models.py +103 -0
- datapilot/core/platforms/dbt/insights/modelling/staging_model_dependent_on_staging_models.py +89 -0
- datapilot/core/platforms/dbt/insights/modelling/unused_sources.py +59 -0
- datapilot/core/platforms/dbt/insights/performance/__init__.py +0 -0
- datapilot/core/platforms/dbt/insights/performance/base.py +26 -0
- datapilot/core/platforms/dbt/insights/performance/chain_view_linking.py +92 -0
- datapilot/core/platforms/dbt/insights/performance/exposure_parent_materializations.py +104 -0
- datapilot/core/platforms/dbt/insights/schema.py +72 -0
- datapilot/core/platforms/dbt/insights/structure/__init__.py +0 -0
- datapilot/core/platforms/dbt/insights/structure/base.py +33 -0
- datapilot/core/platforms/dbt/insights/structure/model_directories_structure.py +92 -0
- datapilot/core/platforms/dbt/insights/structure/model_naming_conventions.py +97 -0
- datapilot/core/platforms/dbt/insights/structure/source_directories_structure.py +80 -0
- datapilot/core/platforms/dbt/insights/structure/test_directory_structure.py +74 -0
- datapilot/core/platforms/dbt/insights/utils.py +9 -0
- datapilot/core/platforms/dbt/schemas/__init__.py +0 -0
- datapilot/core/platforms/dbt/schemas/catalog.py +73 -0
- datapilot/core/platforms/dbt/schemas/manifest.py +462 -0
- datapilot/core/platforms/dbt/utils.py +525 -0
- datapilot/core/platforms/dbt/wrappers/__init__.py +0 -0
- datapilot/core/platforms/dbt/wrappers/catalog/__init__.py +0 -0
- datapilot/core/platforms/dbt/wrappers/catalog/v1/__init__.py +0 -0
- datapilot/core/platforms/dbt/wrappers/catalog/v1/wrapper.py +18 -0
- datapilot/core/platforms/dbt/wrappers/catalog/wrapper.py +9 -0
- datapilot/core/platforms/dbt/wrappers/manifest/__init__.py +0 -0
- datapilot/core/platforms/dbt/wrappers/manifest/v11/__init__.py +0 -0
- datapilot/core/platforms/dbt/wrappers/manifest/v11/schemas.py +47 -0
- datapilot/core/platforms/dbt/wrappers/manifest/v11/wrapper.py +396 -0
- datapilot/core/platforms/dbt/wrappers/manifest/wrapper.py +35 -0
- datapilot/core/platforms/dbt/wrappers/run_results/__init__.py +0 -0
- datapilot/core/platforms/dbt/wrappers/run_results/run_results.py +39 -0
- datapilot/exceptions/__init__.py +0 -0
- datapilot/exceptions/exceptions.py +10 -0
- datapilot/schemas/__init__.py +0 -0
- datapilot/schemas/constants.py +5 -0
- datapilot/schemas/nodes.py +19 -0
- datapilot/schemas/sql.py +10 -0
- datapilot/utils/__init__.py +0 -0
- datapilot/utils/formatting/__init__.py +0 -0
- datapilot/utils/formatting/utils.py +59 -0
- datapilot/utils/utils.py +317 -0
@@ -0,0 +1,129 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from datapilot.core.insights.utils import get_severity
|
4
|
+
from datapilot.core.platforms.dbt.constants import VIEW
|
5
|
+
from datapilot.core.platforms.dbt.insights.checks.base import ChecksInsight
|
6
|
+
from datapilot.core.platforms.dbt.insights.schema import DBTInsightResult
|
7
|
+
from datapilot.core.platforms.dbt.insights.schema import DBTModelInsightResponse
|
8
|
+
|
9
|
+
|
10
|
+
class CheckModelMaterializationByChilds(ChecksInsight):
|
11
|
+
NAME = "Model materialization by children"
|
12
|
+
ALIAS = "check_model_materialization_by_childs"
|
13
|
+
DESCRIPTION = "Fewer children than threshold ideally should be view or ephemeral, more or equal should be table or incremental."
|
14
|
+
REASON_TO_FLAG = "The model is flagged due to inappropriate materialization: models with child counts above the threshold require robust and efficient data processing, hence they should be materialized as tables or incrementals for optimized query performance and data management."
|
15
|
+
THRESHOLD_CHILDS_STR = "threshold_childs"
|
16
|
+
|
17
|
+
def _build_failure_result_view_materialization(
|
18
|
+
self,
|
19
|
+
node_id: str,
|
20
|
+
nr_childs: int,
|
21
|
+
threshold_childs: int,
|
22
|
+
model_materialization: str,
|
23
|
+
) -> DBTInsightResult:
|
24
|
+
"""
|
25
|
+
Build failure result for the insight if a model's materialization is view and has less child models than the threshold.
|
26
|
+
"""
|
27
|
+
|
28
|
+
failure_message = f"The model:{node_id} has {nr_childs} childs, but the materialization is {model_materialization}.\n"
|
29
|
+
|
30
|
+
recommendation = "Consider changing the materialization to table or incremental."
|
31
|
+
|
32
|
+
return DBTInsightResult(
|
33
|
+
type=self.TYPE,
|
34
|
+
name=self.NAME,
|
35
|
+
message=failure_message,
|
36
|
+
recommendation=recommendation,
|
37
|
+
reason_to_flag=self.REASON_TO_FLAG,
|
38
|
+
metadata={"threshold_childs": threshold_childs, "nr_childs": nr_childs, "model_materialization": model_materialization},
|
39
|
+
)
|
40
|
+
|
41
|
+
def _build_failure_result_not_view_materialization(
|
42
|
+
self,
|
43
|
+
node_id: str,
|
44
|
+
nr_childs: int,
|
45
|
+
threshold_childs: int,
|
46
|
+
model_materialization: str,
|
47
|
+
) -> DBTInsightResult:
|
48
|
+
"""
|
49
|
+
Build failure result for the insight if a model's materialization is not view and has more or equal child models than the threshold.
|
50
|
+
"""
|
51
|
+
|
52
|
+
failure_message = f"The model:{node_id} has {nr_childs} childs, but the materialization is {model_materialization}.\n"
|
53
|
+
|
54
|
+
recommendation = "Consider changing the materialization to view or ephemeral."
|
55
|
+
|
56
|
+
return DBTInsightResult(
|
57
|
+
type=self.TYPE,
|
58
|
+
name=self.NAME,
|
59
|
+
message=failure_message,
|
60
|
+
recommendation=recommendation,
|
61
|
+
reason_to_flag=self.REASON_TO_FLAG,
|
62
|
+
metadata={"threshold_childs": threshold_childs, "nr_childs": nr_childs, "model_materialization": model_materialization},
|
63
|
+
)
|
64
|
+
|
65
|
+
def generate(self, *args, **kwargs) -> List[DBTModelInsightResponse]:
|
66
|
+
"""
|
67
|
+
Generate a list of InsightResponse objects for each model in the DBT project,
|
68
|
+
Checks the model materialization by a given threshold of child models.
|
69
|
+
All models with less child models then the treshold should be materialized as views (or ephemerals),
|
70
|
+
all the rest as tables or incrementals.
|
71
|
+
threshold_childs: Threshold from which onwards the materialization should be changed.
|
72
|
+
threshold_childs will be taken from the config file.
|
73
|
+
"""
|
74
|
+
insights = []
|
75
|
+
threshold_childs = self.get_check_config(self.THRESHOLD_CHILDS_STR)
|
76
|
+
if not threshold_childs:
|
77
|
+
self.logger.info(f"Threshold childs are not provided in the configuration file for the insight {self.ALIAS}")
|
78
|
+
return insights
|
79
|
+
|
80
|
+
for node_id, node in self.nodes.items():
|
81
|
+
if self.should_skip_model(node_id):
|
82
|
+
self.logger.debug(f"Skipping model {node_id} as it is not enabled for selected models")
|
83
|
+
continue
|
84
|
+
nr_childs = len(self.children_map.get(node_id, []))
|
85
|
+
model_materialization = node.config.materialized
|
86
|
+
|
87
|
+
if nr_childs > threshold_childs and model_materialization == VIEW:
|
88
|
+
insights.append(
|
89
|
+
DBTModelInsightResponse(
|
90
|
+
unique_id=node_id,
|
91
|
+
package_name=node.package_name,
|
92
|
+
path=node.original_file_path,
|
93
|
+
original_file_path=node.original_file_path,
|
94
|
+
insight=self._build_failure_result_view_materialization(
|
95
|
+
node_id, nr_childs, threshold_childs, model_materialization
|
96
|
+
),
|
97
|
+
severity=get_severity(self.config, self.ALIAS, self.DEFAULT_SEVERITY),
|
98
|
+
)
|
99
|
+
)
|
100
|
+
elif nr_childs <= threshold_childs and model_materialization != VIEW:
|
101
|
+
insights.append(
|
102
|
+
DBTModelInsightResponse(
|
103
|
+
unique_id=node_id,
|
104
|
+
package_name=node.package_name,
|
105
|
+
path=node.original_file_path,
|
106
|
+
original_file_path=node.original_file_path,
|
107
|
+
insight=self._build_failure_result_not_view_materialization(
|
108
|
+
node_id, nr_childs, threshold_childs, model_materialization
|
109
|
+
),
|
110
|
+
severity=get_severity(self.config, self.ALIAS, self.DEFAULT_SEVERITY),
|
111
|
+
)
|
112
|
+
)
|
113
|
+
return insights
|
114
|
+
|
115
|
+
@classmethod
|
116
|
+
def get_config_schema(cls):
|
117
|
+
config_schema = super().get_config_schema()
|
118
|
+
config_schema["config"] = {
|
119
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
120
|
+
"type": "object",
|
121
|
+
"properties": {
|
122
|
+
cls.THRESHOLD_CHILDS_STR: {
|
123
|
+
"type": "integer",
|
124
|
+
"description": "Threshold from which onwards the materialization should be changed.",
|
125
|
+
"default": 5,
|
126
|
+
},
|
127
|
+
},
|
128
|
+
}
|
129
|
+
return config_schema
|
@@ -0,0 +1,132 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict
|
3
|
+
from typing import List
|
4
|
+
|
5
|
+
from datapilot.core.insights.utils import get_severity
|
6
|
+
from datapilot.core.platforms.dbt.insights.checks.base import ChecksInsight
|
7
|
+
from datapilot.core.platforms.dbt.insights.schema import DBTInsightResult
|
8
|
+
from datapilot.core.platforms.dbt.insights.schema import DBTModelInsightResponse
|
9
|
+
from datapilot.core.platforms.dbt.schemas.manifest import AltimateResourceType
|
10
|
+
from datapilot.utils.utils import is_superset_path
|
11
|
+
|
12
|
+
|
13
|
+
class CheckModelNameContract(ChecksInsight):
|
14
|
+
NAME = "Valid Mmdel name by folder"
|
15
|
+
ALIAS = "model_name_by_folder"
|
16
|
+
DESCRIPTION = (
|
17
|
+
"Check that model name abides to a contract (similar to check-column-name-contract). A contract consists of a regex pattern."
|
18
|
+
)
|
19
|
+
REASON_TO_FLAG = "Model naming convention is not as expected"
|
20
|
+
DEFAULT_PATTERN_STR = "default_pattern"
|
21
|
+
PATTERNS_LIST_STR = "patterns"
|
22
|
+
PATTERN_STR = "pattern"
|
23
|
+
FOLDER_STR = "folder"
|
24
|
+
|
25
|
+
def _build_failure_result(
|
26
|
+
self,
|
27
|
+
node_id: str,
|
28
|
+
failure: Dict[str, str],
|
29
|
+
) -> DBTInsightResult:
|
30
|
+
"""
|
31
|
+
Build failure result for the insight if a column has a different name that doesn't match the contract.
|
32
|
+
|
33
|
+
:return: An instance of InsightResult containing failure message and recommendation.
|
34
|
+
"""
|
35
|
+
model_name = failure.get("model_name")
|
36
|
+
model_path = failure.get("model_path")
|
37
|
+
expected_pattern = failure.get("pattern")
|
38
|
+
failure_message = (
|
39
|
+
f"The model:{node_id} with name {model_name} in {model_path} does not match the contract pattern: {expected_pattern}."
|
40
|
+
)
|
41
|
+
|
42
|
+
recommendation = (
|
43
|
+
"Update the model name to adhere to the contract. "
|
44
|
+
"Consistent model naming conventions provide valuable context and aids in data understanding and collaboration."
|
45
|
+
)
|
46
|
+
|
47
|
+
return DBTInsightResult(
|
48
|
+
type=self.TYPE,
|
49
|
+
name=self.NAME,
|
50
|
+
message=failure_message,
|
51
|
+
recommendation=recommendation,
|
52
|
+
reason_to_flag=self.REASON_TO_FLAG,
|
53
|
+
metadata={"model_unique_id": node_id, **failure},
|
54
|
+
)
|
55
|
+
|
56
|
+
def generate(self, *args, **kwargs) -> List[DBTModelInsightResponse]:
|
57
|
+
"""
|
58
|
+
Generate a list of InsightResponse objects for each model in the DBT project,
|
59
|
+
identifying models with model name that matches a certain regex pattern.
|
60
|
+
"""
|
61
|
+
insights = []
|
62
|
+
self.default_pattern = self.get_check_config(self.DEFAULT_PATTERN_STR)
|
63
|
+
pattern_configs = self.get_check_config(self.PATTERNS_LIST_STR)
|
64
|
+
if not pattern_configs:
|
65
|
+
self.logger.debug(f"Model name contract not found in insight config for {self.ALIAS}. Skipping insight.")
|
66
|
+
return []
|
67
|
+
self.patterns = {
|
68
|
+
pattern.get(self.FOLDER_STR): pattern.get(self.PATTERN_STR)
|
69
|
+
for pattern in pattern_configs
|
70
|
+
if pattern.get(self.PATTERN_STR) and pattern.get(self.FOLDER_STR)
|
71
|
+
}
|
72
|
+
for node_id, node in self.nodes.items():
|
73
|
+
if self.should_skip_model(node_id):
|
74
|
+
self.logger.debug(f"Skipping model {node_id} as it is not enabled for selected models")
|
75
|
+
continue
|
76
|
+
if node.resource_type == AltimateResourceType.model:
|
77
|
+
failure = self._check_model_name_contract(node_id)
|
78
|
+
if failure:
|
79
|
+
insights.append(
|
80
|
+
DBTModelInsightResponse(
|
81
|
+
unique_id=node_id,
|
82
|
+
package_name=node.package_name,
|
83
|
+
path=node.original_file_path,
|
84
|
+
original_file_path=node.original_file_path,
|
85
|
+
insight=self._build_failure_result(node_id, failure),
|
86
|
+
severity=get_severity(self.config, self.ALIAS, self.DEFAULT_SEVERITY),
|
87
|
+
)
|
88
|
+
)
|
89
|
+
return insights
|
90
|
+
|
91
|
+
def _check_model_name_contract(self, model_unique_id: str) -> bool:
|
92
|
+
"""
|
93
|
+
Check if the model name abides to the contract.
|
94
|
+
"""
|
95
|
+
model_name = self.get_node(model_unique_id).name
|
96
|
+
model_path = self.get_node(model_unique_id).original_file_path
|
97
|
+
for folder, pattern in self.patterns.items():
|
98
|
+
if is_superset_path(folder, model_path):
|
99
|
+
if re.match(pattern, model_name, re.IGNORECASE) is None:
|
100
|
+
return {"pattern": pattern, "model_name": model_name, "model_path": model_path}
|
101
|
+
return {}
|
102
|
+
|
103
|
+
@classmethod
|
104
|
+
def get_config_schema(cls):
|
105
|
+
config_schema = super().get_config_schema()
|
106
|
+
config_schema["config"] = {
|
107
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
108
|
+
"type": "object",
|
109
|
+
"properties": {
|
110
|
+
cls.DEFAULT_PATTERN_STR: {
|
111
|
+
"type": "string",
|
112
|
+
"description": "The regex pattern to check the model name against",
|
113
|
+
"default": "^[a-z_]+$",
|
114
|
+
},
|
115
|
+
cls.PATTERNS_LIST_STR: {
|
116
|
+
"type": "array",
|
117
|
+
"items": {
|
118
|
+
"type": "object",
|
119
|
+
"properties": {
|
120
|
+
cls.PATTERN_STR: {"type": "string", "description": "The regex pattern to check the model name against"},
|
121
|
+
cls.FOLDER_STR: {"type": "string", "description": "The folder to apply the pattern to."},
|
122
|
+
},
|
123
|
+
"required": [cls.PATTERN_STR, cls.FOLDER_STR],
|
124
|
+
},
|
125
|
+
"description": "A list of regex patterns to check the model name against. Each pattern is applied to the folder specified. If no pattern is found for the folder, the default pattern is used.",
|
126
|
+
"default": [],
|
127
|
+
},
|
128
|
+
},
|
129
|
+
"required": [cls.DEFAULT_PATTERN_STR, cls.PATTERNS_LIST_STR],
|
130
|
+
}
|
131
|
+
config_schema["files_required"] = cls.FILES_REQUIRED
|
132
|
+
return config_schema
|
@@ -0,0 +1,135 @@
|
|
1
|
+
from typing import List
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from datapilot.core.insights.utils import get_severity
|
5
|
+
from datapilot.core.platforms.dbt.insights.checks.base import ChecksInsight
|
6
|
+
from datapilot.core.platforms.dbt.insights.schema import DBTInsightResult
|
7
|
+
from datapilot.core.platforms.dbt.insights.schema import DBTModelInsightResponse
|
8
|
+
from datapilot.core.platforms.dbt.schemas.manifest import AltimateResourceType
|
9
|
+
|
10
|
+
|
11
|
+
class CheckModelParentsAndChilds(ChecksInsight):
|
12
|
+
NAME = "Model has specific number of parents or/and childs"
|
13
|
+
ALIAS = "check_model_parents_and_childs"
|
14
|
+
DESCRIPTION = "Ensures the model has a specific number (max/min) of parents or/and childs."
|
15
|
+
REASON_TO_FLAG = (
|
16
|
+
"Models with a specific number of parents or/and childs can lead to confusion and hinder effective data "
|
17
|
+
"modeling and analysis. It's important to have consistent model relationships."
|
18
|
+
)
|
19
|
+
MIN_PARENTS_STR = "min_parents"
|
20
|
+
MAX_PARENTS_STR = "max_parents"
|
21
|
+
MIN_CHILDS_STR = "min_children"
|
22
|
+
MAX_CHILDS_STR = "max_children"
|
23
|
+
|
24
|
+
def _build_failure_result(
|
25
|
+
self,
|
26
|
+
node_id: str,
|
27
|
+
failure_message: str,
|
28
|
+
) -> DBTInsightResult:
|
29
|
+
"""
|
30
|
+
Build failure result for the insight if a column has specific number (max/min) of parents or/and childs.
|
31
|
+
|
32
|
+
:return: An instance of InsightResult containing failure message and recommendation.
|
33
|
+
"""
|
34
|
+
recommendation = (
|
35
|
+
"Update the model to adhere to have the required number of parents or childs."
|
36
|
+
"Models not following the required number of parents or childs can lead to confusion and hinder effective data "
|
37
|
+
)
|
38
|
+
|
39
|
+
return DBTInsightResult(
|
40
|
+
type=self.TYPE,
|
41
|
+
name=self.NAME,
|
42
|
+
message=failure_message,
|
43
|
+
recommendation=recommendation,
|
44
|
+
reason_to_flag=self.REASON_TO_FLAG,
|
45
|
+
metadata={
|
46
|
+
"min_parents": self.min_parents,
|
47
|
+
"max_parents": self.max_parents,
|
48
|
+
"min_childs": self.min_childs,
|
49
|
+
"max_childs": self.max_childs,
|
50
|
+
"model_unique_id": node_id,
|
51
|
+
},
|
52
|
+
)
|
53
|
+
|
54
|
+
def generate(self, *args, **kwargs) -> List[DBTModelInsightResponse]:
|
55
|
+
"""
|
56
|
+
Generate a list of InsightResponse objects for each model in the DBT project,
|
57
|
+
ensures that the model has a specific number (max/min) of parents or/and childs.
|
58
|
+
The parent and child numbers are defined in the config file.
|
59
|
+
The parent and corresponding child information is present in self.children_map
|
60
|
+
"""
|
61
|
+
insights = []
|
62
|
+
self.min_parents = self.get_check_config(self.MIN_PARENTS_STR) or 1
|
63
|
+
self.max_parents = self.get_check_config(self.MAX_PARENTS_STR)
|
64
|
+
self.min_childs = self.get_check_config(self.MIN_CHILDS_STR) or 0
|
65
|
+
self.max_childs = self.get_check_config(self.MAX_CHILDS_STR)
|
66
|
+
|
67
|
+
if not self.max_childs and not self.max_parents:
|
68
|
+
self.logger.info(
|
69
|
+
"max_children and max_parents are required values in the configuration. Please provide the required values. Skipping the insight."
|
70
|
+
)
|
71
|
+
return insights
|
72
|
+
|
73
|
+
for node_id, node in self.nodes.items():
|
74
|
+
if self.should_skip_model(node_id):
|
75
|
+
self.logger.debug(f"Skipping model {node_id} as it is not enabled for selected models")
|
76
|
+
|
77
|
+
if node.resource_type == AltimateResourceType.model:
|
78
|
+
failure_message = self._check_model_parents_and_childs(node_id)
|
79
|
+
if failure_message:
|
80
|
+
insights.append(
|
81
|
+
DBTModelInsightResponse(
|
82
|
+
unique_id=node_id,
|
83
|
+
package_name=node.package_name,
|
84
|
+
path=node.original_file_path,
|
85
|
+
original_file_path=node.original_file_path,
|
86
|
+
insight=self._build_failure_result(node_id, failure_message),
|
87
|
+
severity=get_severity(self.config, self.ALIAS, self.DEFAULT_SEVERITY),
|
88
|
+
)
|
89
|
+
)
|
90
|
+
return insights
|
91
|
+
|
92
|
+
def _check_model_parents_and_childs(self, model_unique_id: str) -> Optional[str]:
|
93
|
+
"""
|
94
|
+
Check if the model has a specific number (max/min) of parents or/and childs.
|
95
|
+
"""
|
96
|
+
children = self.children_map.get(model_unique_id, [])
|
97
|
+
node = self.get_node(model_unique_id)
|
98
|
+
parents = node.depends_on.nodes
|
99
|
+
message = ""
|
100
|
+
if len(parents) < self.min_parents or len(parents) > self.max_parents:
|
101
|
+
message += f"The model:{model_unique_id} doesn't have the required number of parents.\n Min parents: {self.min_parents}, Max parents: {self.max_parents}. It has f{len(parents)} parents\n"
|
102
|
+
|
103
|
+
if len(children) < self.min_childs or len(children) > self.max_childs:
|
104
|
+
message += f"The model:{model_unique_id} doesn't have the required number of childs.\n Min childs: {self.min_childs}, Max childs: {self.max_childs}. It has f{len(children)} childs\n"
|
105
|
+
|
106
|
+
return message
|
107
|
+
|
108
|
+
@classmethod
|
109
|
+
def get_config_schema(cls):
|
110
|
+
config_schema = super().get_config_schema()
|
111
|
+
config_schema["config"] = {
|
112
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
113
|
+
"type": "object",
|
114
|
+
"properties": {
|
115
|
+
cls.MAX_CHILDS_STR: {"type": "integer", "description": "The maximum number of childs a model can have.", "default": 3},
|
116
|
+
cls.MIN_CHILDS_STR: {
|
117
|
+
"type": "integer",
|
118
|
+
"description": "The minimum number of childs a model can have.",
|
119
|
+
"default": 0,
|
120
|
+
},
|
121
|
+
cls.MAX_PARENTS_STR: {
|
122
|
+
"type": "integer",
|
123
|
+
"description": "The maximum number of parents a model can have.",
|
124
|
+
"default": 3,
|
125
|
+
},
|
126
|
+
cls.MIN_PARENTS_STR: {
|
127
|
+
"type": "integer",
|
128
|
+
"description": "The minimum number of parents a model can have.",
|
129
|
+
"default": 0,
|
130
|
+
},
|
131
|
+
},
|
132
|
+
"required": [cls.MAX_CHILDS_STR, cls.MAX_PARENTS_STR],
|
133
|
+
}
|
134
|
+
config_schema["files_required"] = cls.FILES_REQUIRED
|
135
|
+
return config_schema
|
@@ -0,0 +1,109 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from datapilot.core.insights.utils import get_severity
|
4
|
+
from datapilot.core.platforms.dbt.insights.checks.base import ChecksInsight
|
5
|
+
from datapilot.core.platforms.dbt.insights.schema import DBTInsightResult
|
6
|
+
from datapilot.core.platforms.dbt.insights.schema import DBTModelInsightResponse
|
7
|
+
from datapilot.core.platforms.dbt.schemas.manifest import AltimateResourceType
|
8
|
+
|
9
|
+
|
10
|
+
class CheckModelParentsDatabase(ChecksInsight):
|
11
|
+
NAME = "Check model parents database"
|
12
|
+
ALIAS = "check_model_parents_database"
|
13
|
+
DESCRIPTION = "Ensures the parent models or sources are from certain database."
|
14
|
+
REASON_TO_FLAG = "The model has a different database as parent model or source."
|
15
|
+
WHITELIST_STR = "whitelist"
|
16
|
+
BLACKLIST_STR = "blacklist"
|
17
|
+
|
18
|
+
def _build_failure_result(
|
19
|
+
self,
|
20
|
+
node_id: str,
|
21
|
+
parent_database: str,
|
22
|
+
) -> DBTInsightResult:
|
23
|
+
"""
|
24
|
+
Build failure result for the insight if a model's parent database is not whitelist or in blacklist.
|
25
|
+
"""
|
26
|
+
|
27
|
+
failure_message = f"The model:{node_id}'s parent model's database is not in whitelist or blacklisted:\n"
|
28
|
+
|
29
|
+
recommendation = "Update the parent model's database to adhere to the whitelist or remove the model from the blacklist."
|
30
|
+
|
31
|
+
return DBTInsightResult(
|
32
|
+
type=self.TYPE,
|
33
|
+
name=self.NAME,
|
34
|
+
message=failure_message,
|
35
|
+
recommendation=recommendation,
|
36
|
+
reason_to_flag=self.REASON_TO_FLAG,
|
37
|
+
metadata={"parent_database": parent_database},
|
38
|
+
)
|
39
|
+
|
40
|
+
def generate(self, *args, **kwargs) -> List[DBTModelInsightResponse]:
|
41
|
+
"""
|
42
|
+
Generate a list of InsightResponse objects for each model in the DBT project,
|
43
|
+
ensures the parent models or sources are from certain database.
|
44
|
+
The whitelist and blacklist of databases are defined in the config file.
|
45
|
+
"""
|
46
|
+
insights = []
|
47
|
+
|
48
|
+
self.whitelist = self.get_check_config(self.WHITELIST_STR)
|
49
|
+
self.blacklist = self.get_check_config(self.BLACKLIST_STR) or []
|
50
|
+
|
51
|
+
for node_id in self.nodes.keys():
|
52
|
+
if self.should_skip_model(node_id):
|
53
|
+
self.logger.debug(f"Skipping model {node_id} as it is not enabled for selected models")
|
54
|
+
continue
|
55
|
+
parent_database = self._check_model_parents_database(node_id)
|
56
|
+
if parent_database:
|
57
|
+
insights.append(
|
58
|
+
DBTModelInsightResponse(
|
59
|
+
unique_id=node_id,
|
60
|
+
package_name=self.nodes[node_id].package_name,
|
61
|
+
path=self.nodes[node_id].original_file_path,
|
62
|
+
original_file_path=self.nodes[node_id].original_file_path,
|
63
|
+
insight=self._build_failure_result(node_id, parent_database),
|
64
|
+
severity=get_severity(self.config, self.ALIAS, self.DEFAULT_SEVERITY),
|
65
|
+
)
|
66
|
+
)
|
67
|
+
return insights
|
68
|
+
|
69
|
+
def _check_model_parents_database(self, model_unique_id: str) -> bool:
|
70
|
+
"""
|
71
|
+
Check if the parent models or sources are from certain database.
|
72
|
+
"""
|
73
|
+
model = self.get_node(model_unique_id)
|
74
|
+
if model.resource_type == AltimateResourceType.model:
|
75
|
+
for parent in getattr(model.depends_on, "nodes", []):
|
76
|
+
parent_model = self.get_node(parent)
|
77
|
+
if not parent_model:
|
78
|
+
continue
|
79
|
+
|
80
|
+
if parent_model.resource_type not in [AltimateResourceType.model, AltimateResourceType.source]:
|
81
|
+
continue
|
82
|
+
|
83
|
+
if self.whitelist and (parent_model.database not in self.whitelist):
|
84
|
+
return parent_model.database
|
85
|
+
|
86
|
+
if self.blacklist and (parent_model.database in self.blacklist):
|
87
|
+
return parent_model.database
|
88
|
+
return None
|
89
|
+
|
90
|
+
@classmethod
|
91
|
+
def get_config_schema(cls):
|
92
|
+
config_schema = super().get_config_schema()
|
93
|
+
config_schema["config"] = {
|
94
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
95
|
+
"type": "object",
|
96
|
+
"properties": {
|
97
|
+
cls.WHITELIST_STR: {
|
98
|
+
"type": "array",
|
99
|
+
"items": {"type": "string"},
|
100
|
+
"description": "List of databases that are allowed as parent models or sources.",
|
101
|
+
},
|
102
|
+
cls.BLACKLIST_STR: {
|
103
|
+
"type": "array",
|
104
|
+
"items": {"type": "string"},
|
105
|
+
"description": "List of databases that are not allowed as parent models or sources.",
|
106
|
+
},
|
107
|
+
},
|
108
|
+
}
|
109
|
+
return config_schema
|
@@ -0,0 +1,109 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from datapilot.core.insights.utils import get_severity
|
4
|
+
from datapilot.core.platforms.dbt.insights.checks.base import ChecksInsight
|
5
|
+
from datapilot.core.platforms.dbt.insights.schema import DBTInsightResult
|
6
|
+
from datapilot.core.platforms.dbt.insights.schema import DBTModelInsightResponse
|
7
|
+
from datapilot.core.platforms.dbt.schemas.manifest import AltimateResourceType
|
8
|
+
|
9
|
+
|
10
|
+
class CheckModelParentsSchema(ChecksInsight):
|
11
|
+
NAME = "Model Parents are from an allowed list of schemas"
|
12
|
+
ALIAS = "check_model_parents_schema"
|
13
|
+
DESCRIPTION = "Ensures the parent models or sources are from certain schema."
|
14
|
+
REASON_TO_FLAG = "The model has a different schema as parent model or source."
|
15
|
+
|
16
|
+
WHITELIST_STR = "whitelist"
|
17
|
+
BLACKLIST_STR = "blacklist"
|
18
|
+
|
19
|
+
def _build_failure_result(
|
20
|
+
self,
|
21
|
+
node_id: str,
|
22
|
+
parent_schema: str,
|
23
|
+
) -> DBTInsightResult:
|
24
|
+
"""
|
25
|
+
Build failure result for the insight if a model's parent schema is not whitelist or in blacklist.
|
26
|
+
"""
|
27
|
+
|
28
|
+
failure_message = f"The model:{node_id}'s parent model's schema is not in whitelist or blacklisted:\n"
|
29
|
+
|
30
|
+
recommendation = "Update the parent model's schema to adhere to the whitelist or remove the model from the blacklist."
|
31
|
+
|
32
|
+
return DBTInsightResult(
|
33
|
+
type=self.TYPE,
|
34
|
+
name=self.NAME,
|
35
|
+
message=failure_message,
|
36
|
+
recommendation=recommendation,
|
37
|
+
reason_to_flag=self.REASON_TO_FLAG,
|
38
|
+
metadata={"parent_schema": parent_schema},
|
39
|
+
)
|
40
|
+
|
41
|
+
def generate(self, *args, **kwargs) -> List[DBTModelInsightResponse]:
|
42
|
+
"""
|
43
|
+
Generate a list of InsightResponse objects for each model in the DBT project,
|
44
|
+
ensures the parent models or sources are from certain schema.
|
45
|
+
The whitelist and blacklist of schemas are defined in the config file.
|
46
|
+
"""
|
47
|
+
insights = []
|
48
|
+
self.whitelist = self.get_check_config(self.WHITELIST_STR)
|
49
|
+
self.blacklist = self.get_check_config(self.BLACKLIST_STR) or []
|
50
|
+
|
51
|
+
for node_id in self.nodes.keys():
|
52
|
+
if self.should_skip_model(node_id):
|
53
|
+
self.logger.debug(f"Skipping model {node_id} as it is not enabled for selected models")
|
54
|
+
continue
|
55
|
+
parent_schema = self._check_model_parents_schema(node_id)
|
56
|
+
if parent_schema:
|
57
|
+
insights.append(
|
58
|
+
DBTModelInsightResponse(
|
59
|
+
unique_id=node_id,
|
60
|
+
package_name=self.nodes[node_id].package_name,
|
61
|
+
path=self.nodes[node_id].original_file_path,
|
62
|
+
original_file_path=self.nodes[node_id].original_file_path,
|
63
|
+
insight=self._build_failure_result(node_id, parent_schema),
|
64
|
+
severity=get_severity(self.config, self.ALIAS, self.DEFAULT_SEVERITY),
|
65
|
+
)
|
66
|
+
)
|
67
|
+
return insights
|
68
|
+
|
69
|
+
def _check_model_parents_schema(self, model_unique_id: str) -> bool:
|
70
|
+
"""
|
71
|
+
Check if the parent models or sources are from certain schema.
|
72
|
+
"""
|
73
|
+
model = self.get_node(model_unique_id)
|
74
|
+
if model.resource_type == AltimateResourceType.model:
|
75
|
+
for parent in getattr(model.depends_on, "nodes", []):
|
76
|
+
parent_model = self.get_node(parent)
|
77
|
+
if not parent_model:
|
78
|
+
continue
|
79
|
+
|
80
|
+
if parent_model.resource_type not in [AltimateResourceType.model, AltimateResourceType.source]:
|
81
|
+
continue
|
82
|
+
|
83
|
+
if self.whitelist and (parent_model.schema_name not in self.whitelist):
|
84
|
+
return parent_model.schema_name
|
85
|
+
|
86
|
+
if self.blacklist and (parent_model.schema_name in self.blacklist):
|
87
|
+
return parent_model.schema_name
|
88
|
+
return None
|
89
|
+
|
90
|
+
@classmethod
|
91
|
+
def get_config_schema(cls):
|
92
|
+
config_schema = super().get_config_schema()
|
93
|
+
config_schema["config"] = {
|
94
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
95
|
+
"type": "object",
|
96
|
+
"properties": {
|
97
|
+
cls.WHITELIST_STR: {
|
98
|
+
"type": "array",
|
99
|
+
"items": {"type": "string"},
|
100
|
+
"description": "List of schemas that are allowed as parent models or sources.",
|
101
|
+
},
|
102
|
+
cls.BLACKLIST_STR: {
|
103
|
+
"type": "array",
|
104
|
+
"items": {"type": "string"},
|
105
|
+
"description": "List of schemas that are not allowed as parent models or sources.",
|
106
|
+
},
|
107
|
+
},
|
108
|
+
}
|
109
|
+
return config_schema
|