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,92 @@
|
|
1
|
+
from typing import List
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from datapilot.config.utils import get_regex_configuration
|
5
|
+
from datapilot.core.insights.utils import get_severity
|
6
|
+
from datapilot.core.platforms.dbt.constants import OTHER
|
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.insights.structure.base import DBTStructureInsight
|
10
|
+
from datapilot.core.platforms.dbt.schemas.manifest import AltimateResourceType
|
11
|
+
from datapilot.core.platforms.dbt.utils import _check_model_folder_convention
|
12
|
+
from datapilot.core.platforms.dbt.utils import classify_model_type
|
13
|
+
|
14
|
+
|
15
|
+
class DBTModelDirectoryStructure(DBTStructureInsight):
|
16
|
+
"""
|
17
|
+
DBTModelDirectoryStructure checks if models are placed in the correct directories.
|
18
|
+
"""
|
19
|
+
|
20
|
+
NAME = "Bad model directory structure"
|
21
|
+
ALIAS = "model_directory_structure"
|
22
|
+
DESCRIPTION = "This rule identifies models that are not placed in their correct directories. "
|
23
|
+
REASON_TO_FLAG = (
|
24
|
+
"Placing models in the correct directories is vital for maintaining a structured and "
|
25
|
+
"efficient data warehouse. Incorrectly placed models can lead to confusion, hinder "
|
26
|
+
"discoverability, and complicate maintenance and scaling of the dbt project."
|
27
|
+
)
|
28
|
+
FAILURE_MESSAGE = (
|
29
|
+
"Incorrect Directory Placement Detected: The model `{model_unique_id}` is incorrectly "
|
30
|
+
"placed in the current directory. As a `{model_type}` model, it should be located in "
|
31
|
+
"the `{convention}` directory."
|
32
|
+
)
|
33
|
+
RECOMMENDATION = (
|
34
|
+
"To resolve this issue, please move the model `{model_unique_id}` to the `{convention}` "
|
35
|
+
"directory. This change will align the model's location with the established directory "
|
36
|
+
"structure, improving organization and ease of access in your dbt project."
|
37
|
+
)
|
38
|
+
|
39
|
+
def _build_failure_result(self, model_unique_id: str, model_type: str, convention: Optional[str]) -> DBTInsightResult:
|
40
|
+
failure_message = self.FAILURE_MESSAGE.format(
|
41
|
+
model_unique_id=model_unique_id,
|
42
|
+
model_type=model_type,
|
43
|
+
convention=convention,
|
44
|
+
)
|
45
|
+
return DBTInsightResult(
|
46
|
+
name=self.NAME,
|
47
|
+
type=self.TYPE,
|
48
|
+
message=failure_message,
|
49
|
+
recommendation=self.RECOMMENDATION.format(model_unique_id=model_unique_id, convention=convention),
|
50
|
+
reason_to_flag=self.REASON_TO_FLAG,
|
51
|
+
metadata={
|
52
|
+
"model": model_unique_id,
|
53
|
+
"model_type": model_type,
|
54
|
+
"convention": convention,
|
55
|
+
},
|
56
|
+
)
|
57
|
+
|
58
|
+
def generate(self, *args, **kwargs) -> List[DBTModelInsightResponse]:
|
59
|
+
insights = []
|
60
|
+
regex_configuration = get_regex_configuration(self.config)
|
61
|
+
for node in self.nodes.values():
|
62
|
+
if self.should_skip_model(node.unique_id):
|
63
|
+
self.logger.debug(f"Skipping model {node.unique_id} as it is not enabled for selected models")
|
64
|
+
continue
|
65
|
+
if node.resource_type == AltimateResourceType.model:
|
66
|
+
model_type = classify_model_type(node.name, node.original_file_path, regex_configuration)
|
67
|
+
if model_type == OTHER:
|
68
|
+
continue
|
69
|
+
|
70
|
+
valid_convention, message = _check_model_folder_convention(
|
71
|
+
model_type,
|
72
|
+
node.original_file_path,
|
73
|
+
regex_configuration,
|
74
|
+
node=node,
|
75
|
+
sources=self.sources,
|
76
|
+
)
|
77
|
+
if not valid_convention:
|
78
|
+
insights.append(
|
79
|
+
DBTModelInsightResponse(
|
80
|
+
unique_id=node.unique_id,
|
81
|
+
package_name=node.package_name,
|
82
|
+
path=node.path,
|
83
|
+
original_file_path=node.original_file_path,
|
84
|
+
insight=self._build_failure_result(
|
85
|
+
model_unique_id=node.unique_id,
|
86
|
+
model_type=model_type,
|
87
|
+
convention=message,
|
88
|
+
),
|
89
|
+
severity=get_severity(self.config, self.ALIAS, self.DEFAULT_SEVERITY),
|
90
|
+
)
|
91
|
+
)
|
92
|
+
return insights
|
@@ -0,0 +1,97 @@
|
|
1
|
+
from typing import List
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from datapilot.config.utils import get_regex_configuration
|
5
|
+
from datapilot.core.insights.utils import get_severity
|
6
|
+
from datapilot.core.platforms.dbt.constants import MODEL
|
7
|
+
from datapilot.core.platforms.dbt.constants import OTHER
|
8
|
+
from datapilot.core.platforms.dbt.insights.schema import DBTInsightResult
|
9
|
+
from datapilot.core.platforms.dbt.insights.schema import DBTModelInsightResponse
|
10
|
+
from datapilot.core.platforms.dbt.insights.structure.base import DBTStructureInsight
|
11
|
+
from datapilot.core.platforms.dbt.schemas.manifest import AltimateResourceType
|
12
|
+
from datapilot.core.platforms.dbt.utils import _check_model_naming_convention
|
13
|
+
from datapilot.core.platforms.dbt.utils import classify_model_type
|
14
|
+
|
15
|
+
|
16
|
+
class DBTModelNamingConvention(DBTStructureInsight):
|
17
|
+
"""
|
18
|
+
DBTModelNamingConvention identifies models that do not follow the naming convention.
|
19
|
+
"""
|
20
|
+
|
21
|
+
NAME = "Bad model naming convention"
|
22
|
+
ALIAS = "model_naming_convention_check"
|
23
|
+
DESCRIPTION = "This rule identifies models that do not follow the naming convention."
|
24
|
+
REASON_TO_FLAG = (
|
25
|
+
"Inconsistent or unclear naming conventions can lead to confusion and errors in querying the data warehouse. "
|
26
|
+
"A well-defined naming convention clarifies the model type and purpose, promoting better understanding "
|
27
|
+
"and effective data management. This rule flags models that deviate from established naming standards."
|
28
|
+
)
|
29
|
+
FAILURE_MESSAGE = (
|
30
|
+
"Naming Convention Violation Detected: The model `{model_unique_id}` does not comply with the "
|
31
|
+
"established naming convention. It is identified as a `{model_type}` model, but its name does not "
|
32
|
+
"reflect the required prefix or convention `{convention}`. Please update the model name to align "
|
33
|
+
"with the naming standards."
|
34
|
+
)
|
35
|
+
RECOMMENDATION = "Please rename the model `{model_unique_id}` to follow the appropriate naming convention. "
|
36
|
+
|
37
|
+
def _build_failure_result(self, model_unique_id: str, model_type: str, convention: Optional[str]) -> DBTInsightResult:
|
38
|
+
if model_type != OTHER:
|
39
|
+
failure_message = self.FAILURE_MESSAGE.format(
|
40
|
+
model_unique_id=model_unique_id,
|
41
|
+
model_type=model_type,
|
42
|
+
convention=convention,
|
43
|
+
)
|
44
|
+
else:
|
45
|
+
failure_message = (
|
46
|
+
f"The model `{model_unique_id}` was not classified as any of the known model types. "
|
47
|
+
"The naming conventions for it may not be appropriate"
|
48
|
+
)
|
49
|
+
|
50
|
+
return DBTInsightResult(
|
51
|
+
name=self.NAME,
|
52
|
+
type=self.TYPE,
|
53
|
+
message=failure_message,
|
54
|
+
recommendation=self.RECOMMENDATION.format(model_unique_id=model_unique_id),
|
55
|
+
reason_to_flag=self.REASON_TO_FLAG,
|
56
|
+
metadata={
|
57
|
+
"model": model_unique_id,
|
58
|
+
"model_type": model_type,
|
59
|
+
"convention": convention,
|
60
|
+
},
|
61
|
+
)
|
62
|
+
|
63
|
+
def generate(self, *args, **kwargs) -> List[DBTModelInsightResponse]:
|
64
|
+
insights = []
|
65
|
+
regex_configuration = get_regex_configuration(self.config)
|
66
|
+
for node in self.nodes.values():
|
67
|
+
if self.should_skip_model(node.unique_id):
|
68
|
+
self.logger.debug(f"Skipping model {node.unique_id} as it is not enabled for selected models")
|
69
|
+
continue
|
70
|
+
if node.resource_type == AltimateResourceType.model:
|
71
|
+
model_type = classify_model_type(node.name, node.original_file_path, regex_configuration)
|
72
|
+
if model_type == OTHER:
|
73
|
+
insights.append(
|
74
|
+
DBTModelInsightResponse(
|
75
|
+
unique_id=node.unique_id,
|
76
|
+
package_name=node.package_name,
|
77
|
+
path=node.path,
|
78
|
+
original_file_path=node.original_file_path,
|
79
|
+
insight=self._build_failure_result(node.unique_id, model_type, None),
|
80
|
+
severity=get_severity(self.config, self.ALIAS, self.DEFAULT_SEVERITY),
|
81
|
+
)
|
82
|
+
)
|
83
|
+
continue
|
84
|
+
valid_name, expected_model_type = _check_model_naming_convention(node.name, model_type, regex_configuration.get(MODEL))
|
85
|
+
if not valid_name:
|
86
|
+
insight_result = self._build_failure_result(node.unique_id, model_type, expected_model_type)
|
87
|
+
insights.append(
|
88
|
+
DBTModelInsightResponse(
|
89
|
+
unique_id=node.unique_id,
|
90
|
+
package_name=node.package_name,
|
91
|
+
path=node.path,
|
92
|
+
original_file_path=node.original_file_path,
|
93
|
+
insight=insight_result,
|
94
|
+
severity=get_severity(self.config, self.ALIAS, self.DEFAULT_SEVERITY),
|
95
|
+
)
|
96
|
+
)
|
97
|
+
return insights
|
@@ -0,0 +1,80 @@
|
|
1
|
+
from typing import List
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from datapilot.config.utils import get_regex_configuration
|
5
|
+
from datapilot.core.insights.utils import get_severity
|
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.insights.structure.base import DBTStructureInsight
|
9
|
+
from datapilot.core.platforms.dbt.utils import _check_source_folder_convention
|
10
|
+
|
11
|
+
|
12
|
+
class DBTSourceDirectoryStructure(DBTStructureInsight):
|
13
|
+
"""
|
14
|
+
DBTSourcesDirectoryStructure checks if sources are placed in the correct directories.
|
15
|
+
"""
|
16
|
+
|
17
|
+
NAME = "Bad source directory structure"
|
18
|
+
ALIAS = "source_directory_structure"
|
19
|
+
DESCRIPTION = "This rule identifies sources that are not placed in their correct directories. "
|
20
|
+
REASON_TO_FLAG = (
|
21
|
+
"Sources need to be organized in the correct directories to ensure an efficient and "
|
22
|
+
"maintainable data architecture. Proper directory structure facilitates easy navigation, "
|
23
|
+
"improves readability, and aids in managing the data sources effectively."
|
24
|
+
)
|
25
|
+
FAILURE_MESSAGE = (
|
26
|
+
"Inappropriate Directory Placement Detected: The source file for {source_id} is currently "
|
27
|
+
"placed in an incorrect directory. This can lead to organizational issues and hinder "
|
28
|
+
"efficient source management."
|
29
|
+
)
|
30
|
+
RECOMMENDATION = (
|
31
|
+
"To address this issue, please move the source file for {source_id} to the appropriate "
|
32
|
+
"directory. The recommended directory structure is {convention}, which aligns with best "
|
33
|
+
"practices for organizing source files in dbt projects."
|
34
|
+
)
|
35
|
+
|
36
|
+
def _build_failure_result(self, model_unique_id: str, convention: Optional[str]) -> DBTInsightResult:
|
37
|
+
failure_message = self.FAILURE_MESSAGE.format(
|
38
|
+
source_id=model_unique_id,
|
39
|
+
)
|
40
|
+
return DBTInsightResult(
|
41
|
+
name=self.NAME,
|
42
|
+
type=self.TYPE,
|
43
|
+
message=failure_message,
|
44
|
+
recommendation=self.RECOMMENDATION.format(source_id=model_unique_id, convention=convention),
|
45
|
+
reason_to_flag=self.REASON_TO_FLAG,
|
46
|
+
metadata={
|
47
|
+
"source_id": model_unique_id,
|
48
|
+
"convention": convention,
|
49
|
+
},
|
50
|
+
)
|
51
|
+
|
52
|
+
def generate(self, *args, **kwargs) -> List[DBTModelInsightResponse]:
|
53
|
+
insights = []
|
54
|
+
regex_configuration = get_regex_configuration(self.config)
|
55
|
+
for source_id, source in self.sources.items():
|
56
|
+
if self.should_skip_model(source_id):
|
57
|
+
self.logger.debug(f"Skipping model {source_id} as it is not enabled for selected models")
|
58
|
+
continue
|
59
|
+
valid_convention, expected_directory = _check_source_folder_convention(
|
60
|
+
source_name=source.source_name,
|
61
|
+
folder_path=source.original_file_path,
|
62
|
+
patterns=regex_configuration,
|
63
|
+
)
|
64
|
+
if not valid_convention:
|
65
|
+
insight = self._build_failure_result(
|
66
|
+
model_unique_id=source_id,
|
67
|
+
convention=expected_directory,
|
68
|
+
)
|
69
|
+
insights.append(
|
70
|
+
DBTModelInsightResponse(
|
71
|
+
unique_id=source.unique_id,
|
72
|
+
package_name=source.package_name,
|
73
|
+
path=source.path,
|
74
|
+
original_file_path=source.original_file_path,
|
75
|
+
insight=insight,
|
76
|
+
severity=get_severity(self.config, self.ALIAS, self.DEFAULT_SEVERITY),
|
77
|
+
)
|
78
|
+
)
|
79
|
+
|
80
|
+
return insights
|
@@ -0,0 +1,74 @@
|
|
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.schema import DBTInsightResult
|
6
|
+
from datapilot.core.platforms.dbt.insights.schema import DBTModelInsightResponse
|
7
|
+
from datapilot.core.platforms.dbt.insights.structure.base import DBTStructureInsight
|
8
|
+
from datapilot.utils.utils import get_dir_path
|
9
|
+
|
10
|
+
|
11
|
+
class DBTTestDirectoryStructure(DBTStructureInsight):
|
12
|
+
"""
|
13
|
+
DBTTestDirectoryStructure checks if tests are placed in the correct directories.
|
14
|
+
"""
|
15
|
+
|
16
|
+
NAME = "Bad test directory structure"
|
17
|
+
ALIAS = "test_directory_structure"
|
18
|
+
DESCRIPTION = "This rule checks if tests are correctly placed in the same directories as their corresponding models."
|
19
|
+
REASON_TO_FLAG = (
|
20
|
+
"It is important for tests to be placed in the same directory as their corresponding models to maintain "
|
21
|
+
"a coherent and easy-to-navigate project structure. This practice enhances the ease of understanding "
|
22
|
+
"and updating tests in parallel with model changes."
|
23
|
+
)
|
24
|
+
FAILURE_MESSAGE = (
|
25
|
+
"Incorrect Test Placement Detected: The test `{model_unique_id}` is not in the correct directory. "
|
26
|
+
"For consistent project structure and easy maintenance, it should be placed in the same directory as "
|
27
|
+
"its corresponding model."
|
28
|
+
)
|
29
|
+
RECOMMENDATION = (
|
30
|
+
"To rectify this, move the test `{model_unique_id}` to the directory `{convention}`, where its corresponding "
|
31
|
+
"model is located. This adjustment will align your test's location with best practices for"
|
32
|
+
" project organization."
|
33
|
+
)
|
34
|
+
|
35
|
+
def _build_failure_result(self, model_unique_id: str, convention: Optional[str]) -> DBTInsightResult:
|
36
|
+
failure_message = self.FAILURE_MESSAGE.format(
|
37
|
+
model_unique_id=model_unique_id,
|
38
|
+
)
|
39
|
+
return DBTInsightResult(
|
40
|
+
name=self.NAME,
|
41
|
+
type=self.TYPE,
|
42
|
+
message=failure_message,
|
43
|
+
recommendation=self.RECOMMENDATION.format(model_unique_id=model_unique_id, convention=convention),
|
44
|
+
reason_to_flag=self.REASON_TO_FLAG,
|
45
|
+
metadata={
|
46
|
+
"model": model_unique_id,
|
47
|
+
"convention": convention,
|
48
|
+
},
|
49
|
+
)
|
50
|
+
|
51
|
+
def generate(self, *args, **kwargs) -> List[DBTModelInsightResponse]:
|
52
|
+
insights = []
|
53
|
+
for test_id, test in self.tests.items():
|
54
|
+
if self.should_skip_model(test_id):
|
55
|
+
self.logger.debug(f"Skipping model {test_id} as it is not enabled for selected models")
|
56
|
+
continue
|
57
|
+
test_file_path = get_dir_path(test_id)
|
58
|
+
for node_id in test.depends_on.nodes:
|
59
|
+
node = self.get_node(node_id)
|
60
|
+
if not node:
|
61
|
+
continue
|
62
|
+
expected_dir_path = get_dir_path(node_id)
|
63
|
+
if expected_dir_path != test_file_path:
|
64
|
+
insights.append(
|
65
|
+
DBTModelInsightResponse(
|
66
|
+
unique_id=test_id,
|
67
|
+
package_name=test.package_name,
|
68
|
+
path=test.path,
|
69
|
+
original_file_path=test.original_file_path,
|
70
|
+
insight=self._build_failure_result(test_id, expected_dir_path),
|
71
|
+
severity=get_severity(self.config, self.ALIAS, self.DEFAULT_SEVERITY),
|
72
|
+
)
|
73
|
+
)
|
74
|
+
return insights
|
@@ -0,0 +1,9 @@
|
|
1
|
+
from datapilot.core.platforms.dbt.insights import INSIGHTS
|
2
|
+
|
3
|
+
|
4
|
+
def get_insight_with_configs():
|
5
|
+
return [insight.get_config_schema() for insight in INSIGHTS]
|
6
|
+
|
7
|
+
|
8
|
+
def insights_require_catalog(insights):
|
9
|
+
return any(insight.requires_catalog() for insight in insights)
|
File without changes
|
@@ -0,0 +1,73 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
from typing import ClassVar
|
3
|
+
from typing import Dict
|
4
|
+
from typing import List
|
5
|
+
from typing import Optional
|
6
|
+
from typing import Union
|
7
|
+
|
8
|
+
from pydantic.config import Extra
|
9
|
+
from pydantic.main import BaseModel
|
10
|
+
|
11
|
+
|
12
|
+
class AltimateCatalogMetadata(BaseModel):
|
13
|
+
class Config:
|
14
|
+
extra = Extra.forbid
|
15
|
+
|
16
|
+
dbt_schema_version: Optional[str] = "https://schemas.getdbt.com/dbt/catalog/v1.json"
|
17
|
+
dbt_version: Optional[str] = "0.19.0"
|
18
|
+
generated_at: Optional[datetime] = "2021-02-10T04:42:33.680487Z"
|
19
|
+
invocation_id: Optional[Optional[str]] = None
|
20
|
+
env: ClassVar[Optional[Dict[str, str]]] = {}
|
21
|
+
|
22
|
+
|
23
|
+
class AltimateCatalogTableMetadata(BaseModel):
|
24
|
+
class Config:
|
25
|
+
extra = Extra.forbid
|
26
|
+
|
27
|
+
type: str
|
28
|
+
database: Optional[Optional[str]] = None
|
29
|
+
schema_name: str
|
30
|
+
name: str
|
31
|
+
comment: Optional[Optional[str]] = None
|
32
|
+
owner: Optional[Optional[str]] = None
|
33
|
+
|
34
|
+
|
35
|
+
class AltimateCatalogColumnMetadata(BaseModel):
|
36
|
+
class Config:
|
37
|
+
extra = Extra.forbid
|
38
|
+
|
39
|
+
type: str
|
40
|
+
comment: Optional[Optional[str]] = None
|
41
|
+
index: int
|
42
|
+
name: str
|
43
|
+
|
44
|
+
|
45
|
+
class AltimateCatalogStatsItem(BaseModel):
|
46
|
+
class Config:
|
47
|
+
extra = Extra.forbid
|
48
|
+
|
49
|
+
id: str
|
50
|
+
label: str
|
51
|
+
value: Optional[Optional[Union[bool, str, float]]] = None
|
52
|
+
description: Optional[Optional[str]] = None
|
53
|
+
include: bool
|
54
|
+
|
55
|
+
|
56
|
+
class AltimateCatalogTable(BaseModel):
|
57
|
+
class Config:
|
58
|
+
extra = Extra.forbid
|
59
|
+
|
60
|
+
metadata: AltimateCatalogTableMetadata
|
61
|
+
columns: Dict[str, AltimateCatalogColumnMetadata]
|
62
|
+
stats: Dict[str, AltimateCatalogStatsItem]
|
63
|
+
unique_id: Optional[Optional[str]] = None
|
64
|
+
|
65
|
+
|
66
|
+
class AltimateCatalogCatalogV1(BaseModel):
|
67
|
+
class Config:
|
68
|
+
extra = Extra.forbid
|
69
|
+
|
70
|
+
metadata: AltimateCatalogMetadata
|
71
|
+
nodes: Dict[str, AltimateCatalogTable]
|
72
|
+
sources: Dict[str, AltimateCatalogTable]
|
73
|
+
errors: Optional[Optional[List[str]]] = None
|