dbt-autofix 0.1.0__tar.gz

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.
Files changed (38) hide show
  1. dbt_autofix-0.1.0/PKG-INFO +69 -0
  2. dbt_autofix-0.1.0/README.md +49 -0
  3. dbt_autofix-0.1.0/dbt_autofix/__init__.py +0 -0
  4. dbt_autofix-0.1.0/dbt_autofix/duplicate_keys.py +127 -0
  5. dbt_autofix-0.1.0/dbt_autofix/fields_properties_configs.py +328 -0
  6. dbt_autofix-0.1.0/dbt_autofix/main.py +60 -0
  7. dbt_autofix-0.1.0/dbt_autofix/refactor.py +701 -0
  8. dbt_autofix-0.1.0/dbt_autofix/retrieve_schemas.py +134 -0
  9. dbt_autofix-0.1.0/pyproject.toml +83 -0
  10. dbt_autofix-0.1.0/tests/integration_tests/.gitignore +1 -0
  11. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1/.gitignore +4 -0
  12. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1/README.md +15 -0
  13. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1/analyses/.gitkeep +0 -0
  14. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1/dbt_project.yml +36 -0
  15. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1/macros/.gitkeep +0 -0
  16. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1/models/example/my_first_dbt_model.sql +27 -0
  17. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1/models/example/my_second_dbt_model.sql +6 -0
  18. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1/models/example/yml_with_anchors.yml +21 -0
  19. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1/models/how_to.md +14 -0
  20. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1/seeds/.gitkeep +0 -0
  21. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1/snapshots/.gitkeep +0 -0
  22. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1/tests/.gitkeep +0 -0
  23. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1_expected/.gitignore +4 -0
  24. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1_expected/README.md +15 -0
  25. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1_expected/analyses/.gitkeep +0 -0
  26. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1_expected/dbt_project.yml +36 -0
  27. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1_expected/macros/.gitkeep +0 -0
  28. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1_expected/models/example/my_first_dbt_model.sql +27 -0
  29. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1_expected/models/example/my_second_dbt_model.sql +6 -0
  30. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1_expected/models/example/yml_with_anchors.yml +21 -0
  31. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1_expected/models/how_to.md +14 -0
  32. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1_expected/seeds/.gitkeep +0 -0
  33. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1_expected/snapshots/.gitkeep +0 -0
  34. dbt_autofix-0.1.0/tests/integration_tests/dbt_projects/project1_expected/tests/.gitkeep +0 -0
  35. dbt_autofix-0.1.0/tests/integration_tests/test_full_dbt_projects.py +75 -0
  36. dbt_autofix-0.1.0/tests/unit_tests/test_check_fields_no_overlap.py +21 -0
  37. dbt_autofix-0.1.0/tests/unit_tests/test_duplicate_keys.py +139 -0
  38. dbt_autofix-0.1.0/tests/unit_tests/test_refactor.py +690 -0
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.1
2
+ Name: dbt-autofix
3
+ Version: 0.1.0
4
+ Summary: CLI to autofix deprecations in dbt projects
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: pyyaml>=6.0.2
7
+ Requires-Dist: typer>=0.15.2
8
+ Requires-Dist: yamllint>=1.37.0
9
+ Requires-Dist: rich>=13.7.0
10
+ Requires-Dist: click<8.2.0
11
+ Requires-Dist: ruamel-yaml>=0.18.10
12
+ Requires-Dist: ruamel-yaml-string>=0.1.1
13
+ Requires-Dist: httpx>=0.28.1
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest>=7.2.0; extra == "test"
16
+ Requires-Dist: pytest-cov>=4.1.0; extra == "test"
17
+ Requires-Dist: pre-commit>=4.2.0; extra == "test"
18
+ Requires-Dist: nox; extra == "test"
19
+ Description-Content-Type: text/markdown
20
+
21
+ # dbt-autofix (previously dbt-cleanup)
22
+
23
+ This tool can help teams clean up their dbt projects so that it conforms with dbt configuration best practices and update deprecated config:
24
+
25
+
26
+ | Deprecation Code in dbt Core | Files | Description |
27
+ | ---------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------ |
28
+ | `CustomKeyInObjectDeprecation` (*) | YAML files | Move all models configs under `config:` in YAML files |
29
+ | `CustomKeyInObjectDeprecation` (*) | YAML files | Move all models extra config (not valid or custom) under `meta:` and `meta` under `config:` |
30
+ | `DuplicateYAMLKeysDeprecation` | YAML files | Remove duplicate keys in YAML files, keeping the second one to keep the same behaviour |
31
+ | `UnexpectedJinjaBlockDeprecation` | SQL files | Remove extra `{% endmacro %}` and `{% endif %}` that don't have corresponding opening statements |
32
+ | - | `dbt_project.yml` | Prefix all configs for modeles/tests etc... with a `+` |
33
+ | `ConfigDataPathDeprecation` | `dbt_project.yml` | Remove deprecated config for data path (now seed) |
34
+ | `ConfigLogPathDeprecation` | `dbt_project.yml` | Remove deprecated config for log path |
35
+ | `ConfigSourcePathDeprecation` | `dbt_project.yml` | Remove deprecated config for source path |
36
+ | `ConfigTargetPathDeprecation` | `dbt_project.yml` | Remove deprecated config for target path |
37
+
38
+ (*) : those autofix rules are currently deactivated in `main`
39
+
40
+ ## Installation
41
+
42
+ To run it from the git repo directly, install `uv` and then
43
+
44
+ run the tool directly
45
+ ```sh
46
+ uvx --from git+https://github.com/dbt-labs/dbt-autofix.git dbt-autofix --help
47
+ ```
48
+
49
+ or install it so that it can be run with `dbt-cleanup` in the future
50
+ ```sh
51
+ uv tool install --from git+https://github.com/dbt-labs/dbt-autofix.git dbt-autofix
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ - `dbt-autofix deprecations`: refactor YAML and SQL files to fix some deprecations
57
+ - add `--path <mypath>` to configure the path of the dbt project (defaults to `.`)
58
+ - add `--dry-run` for running in dry run mode
59
+ - add `--json` to get resulting data in a JSONL format
60
+
61
+ Each JSON object will have the following keys:
62
+
63
+ - "mode": "applied" or "dry_run"
64
+ - "file_path": the full path of the file modified. Each file will appear only once
65
+ - "refactors": the list of refactoring rules applied
66
+
67
+ Calling `deprecations` without `--dry-run` should be safe if your dbt code is part of a git repo.
68
+
69
+ Please review the suggested changes to your dbt project before merging to `main` and make those changes go through your typical CI/CD process.
@@ -0,0 +1,49 @@
1
+ # dbt-autofix (previously dbt-cleanup)
2
+
3
+ This tool can help teams clean up their dbt projects so that it conforms with dbt configuration best practices and update deprecated config:
4
+
5
+
6
+ | Deprecation Code in dbt Core | Files | Description |
7
+ | ---------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------ |
8
+ | `CustomKeyInObjectDeprecation` (*) | YAML files | Move all models configs under `config:` in YAML files |
9
+ | `CustomKeyInObjectDeprecation` (*) | YAML files | Move all models extra config (not valid or custom) under `meta:` and `meta` under `config:` |
10
+ | `DuplicateYAMLKeysDeprecation` | YAML files | Remove duplicate keys in YAML files, keeping the second one to keep the same behaviour |
11
+ | `UnexpectedJinjaBlockDeprecation` | SQL files | Remove extra `{% endmacro %}` and `{% endif %}` that don't have corresponding opening statements |
12
+ | - | `dbt_project.yml` | Prefix all configs for modeles/tests etc... with a `+` |
13
+ | `ConfigDataPathDeprecation` | `dbt_project.yml` | Remove deprecated config for data path (now seed) |
14
+ | `ConfigLogPathDeprecation` | `dbt_project.yml` | Remove deprecated config for log path |
15
+ | `ConfigSourcePathDeprecation` | `dbt_project.yml` | Remove deprecated config for source path |
16
+ | `ConfigTargetPathDeprecation` | `dbt_project.yml` | Remove deprecated config for target path |
17
+
18
+ (*) : those autofix rules are currently deactivated in `main`
19
+
20
+ ## Installation
21
+
22
+ To run it from the git repo directly, install `uv` and then
23
+
24
+ run the tool directly
25
+ ```sh
26
+ uvx --from git+https://github.com/dbt-labs/dbt-autofix.git dbt-autofix --help
27
+ ```
28
+
29
+ or install it so that it can be run with `dbt-cleanup` in the future
30
+ ```sh
31
+ uv tool install --from git+https://github.com/dbt-labs/dbt-autofix.git dbt-autofix
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ - `dbt-autofix deprecations`: refactor YAML and SQL files to fix some deprecations
37
+ - add `--path <mypath>` to configure the path of the dbt project (defaults to `.`)
38
+ - add `--dry-run` for running in dry run mode
39
+ - add `--json` to get resulting data in a JSONL format
40
+
41
+ Each JSON object will have the following keys:
42
+
43
+ - "mode": "applied" or "dry_run"
44
+ - "file_path": the full path of the file modified. Each file will appear only once
45
+ - "refactors": the list of refactoring rules applied
46
+
47
+ Calling `deprecations` without `--dry-run` should be safe if your dbt code is part of a git repo.
48
+
49
+ Please review the suggested changes to your dbt project before merging to `main` and make those changes go through your typical CI/CD process.
File without changes
@@ -0,0 +1,127 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+ from typing import List, Tuple
4
+
5
+ import yaml
6
+ import yamllint.config
7
+ import yamllint.linter
8
+ from rich.console import Console
9
+
10
+ from dbt_autofix.refactor import DbtYAML
11
+
12
+ console = Console()
13
+
14
+ config = """
15
+ rules:
16
+ key-duplicates: enable
17
+ """
18
+
19
+ yaml_config = yamllint.config.YamlLintConfig(config)
20
+
21
+
22
+ @dataclass
23
+ class DuplicateFound:
24
+ file: Path
25
+ line: int
26
+ key: str
27
+ value: str
28
+
29
+ def __str__(self):
30
+ return f"{self.file}:{self.line} -- {self.value}"
31
+
32
+
33
+ def find_duplicate_keys(
34
+ root_dir: Path,
35
+ dry_run: bool = False,
36
+ ) -> Tuple[List[DuplicateFound], List[DuplicateFound]]:
37
+ """
38
+ Find duplicate keys in the project and packages.
39
+ """
40
+ project_duplicates: List[DuplicateFound] = []
41
+ package_duplicates: List[DuplicateFound] = []
42
+
43
+ yml_files = set(root_dir.glob("**/*.yml")).union(set(root_dir.glob("**/*.yaml")))
44
+ yml_files_target = set((root_dir / "target").glob("**/*.yml")).union(set((root_dir / "target").glob("**/*.yaml")))
45
+
46
+ packages_path = yaml.safe_load((root_dir / "dbt_project.yml").read_text()).get(
47
+ "packages-install-path", "dbt_packages"
48
+ )
49
+
50
+ yml_files_packages = set((root_dir / packages_path).glob("**/*.yml")).union(
51
+ set((root_dir / packages_path).glob("**/*.yaml"))
52
+ )
53
+
54
+ # this is a hack to avoid checking integration_tests. it won't work everywhere but it's good enough for now
55
+ yml_files_packages_integration_tests = set((root_dir / packages_path).glob("**/integration_tests/**/*.yml")).union(
56
+ set((root_dir / packages_path).glob("**/integration_tests/**/*.yaml"))
57
+ )
58
+ yml_files_packages_not_integration_tests = yml_files_packages - yml_files_packages_integration_tests
59
+
60
+ yml_files_not_target_or_packages = yml_files - yml_files_target - yml_files_packages
61
+
62
+ # Check project YML files
63
+ for file in yml_files_not_target_or_packages:
64
+ file_with_duplicate = False
65
+ file_content = file.read_text()
66
+ for p in yamllint.linter.run(file_content, yaml_config):
67
+ if p.rule == "key-duplicates":
68
+ file_with_duplicate = True
69
+ project_duplicates.append(
70
+ DuplicateFound(
71
+ file=file,
72
+ line=p.line,
73
+ key=p.desc.split('"')[1] if '"' in p.desc else "", # Extract key from description
74
+ value=p.desc,
75
+ )
76
+ )
77
+ if file_with_duplicate and not dry_run:
78
+ without_duplicates = yaml.safe_load(file_content)
79
+ ruamel_yaml = DbtYAML()
80
+ ruamel_yaml.dump_to_string(without_duplicates) # type: ignore
81
+
82
+ # Check package YML files
83
+ for file in yml_files_packages_not_integration_tests:
84
+ file_content = file.read_text()
85
+ for p in yamllint.linter.run(file_content, yaml_config):
86
+ if p.rule == "key-duplicates":
87
+ package_duplicates.append(
88
+ DuplicateFound(
89
+ file=file,
90
+ line=p.line,
91
+ key=p.desc.split('"')[1] if '"' in p.desc else "", # Extract key from description
92
+ value=p.desc,
93
+ )
94
+ )
95
+
96
+ return project_duplicates, package_duplicates
97
+
98
+
99
+ def print_duplicate_keys(project_duplicates: List[DuplicateFound], package_duplicates: List[DuplicateFound]) -> None:
100
+ """
101
+ Print duplicate keys in the project and packages as well as instructions to fix them.
102
+ """
103
+ if not project_duplicates and not package_duplicates:
104
+ return
105
+
106
+ if project_duplicates:
107
+ console.print("\nThere are issues in your project YML files", style="bold red")
108
+ console.print(
109
+ (
110
+ "Please remove duplicates by hand. dbt's default behavior is to keep the last occurence of a key.\n"
111
+ "If you want to keep the same behaviour remove or comments lines found for the same key and before in the file.\n"
112
+ "Once you have done all the changes in the files, run the tool again.\n"
113
+ )
114
+ )
115
+ for dup in project_duplicates:
116
+ console.print(str(dup))
117
+
118
+ if package_duplicates:
119
+ console.print(
120
+ (
121
+ "\nThose packages might have issues. If those are not maintained by you, check if there are updates available. "
122
+ "If they are private packages, remove duplicates in their own repository and publish a new version.\n"
123
+ ),
124
+ style="bold red",
125
+ )
126
+ for dup in package_duplicates:
127
+ console.print(str(dup))
@@ -0,0 +1,328 @@
1
+ import csv
2
+ from dataclasses import dataclass
3
+
4
+ from rich import print
5
+
6
+
7
+ @dataclass
8
+ class AllowedConfig:
9
+ allowed_config_fields: set[str]
10
+ allowed_properties: set[str]
11
+
12
+ def __post_init__(self):
13
+ self.allowed_config_fields_without_meta = self.allowed_config_fields - {"meta"}
14
+ # in case we forgot to remove meta from the allowed properties
15
+ self.allowed_properties = self.allowed_properties - {"meta"}
16
+
17
+
18
+ models_allowed_config = AllowedConfig(
19
+ allowed_config_fields=set(
20
+ [
21
+ # model specific
22
+ "materialized",
23
+ "sql_header",
24
+ "on_configuration_change",
25
+ "unique_key",
26
+ "incremental_strategy", # was missing at first
27
+ "on_schema_change", # was missing at first
28
+ "batch_size",
29
+ "begin",
30
+ "lookback",
31
+ "concurrent_batches",
32
+ # general
33
+ "enabled",
34
+ "tags",
35
+ "pre_hook",
36
+ "post_hook",
37
+ "database",
38
+ "schema",
39
+ "alias",
40
+ "persist_docs",
41
+ "meta",
42
+ "grants",
43
+ "contract",
44
+ "event_time",
45
+ # Snowflake: https://github.com/dbt-labs/dbt-adapters/blob/af33935b119347cc021554ea854884bce986ef8d/dbt-snowflake/src/dbt/adapters/snowflake/impl.py#L42-L58
46
+ "transient",
47
+ "cluster_by",
48
+ "automatic_clustering",
49
+ "secure",
50
+ "copy_grants",
51
+ "snowflake_warehouse",
52
+ "query_tag",
53
+ "tmp_relation_type",
54
+ "merge_update_columns",
55
+ "target_lag",
56
+ "table_format",
57
+ "external_volume",
58
+ "base_location_root",
59
+ "base_location_subpath",
60
+ # BQ: https://github.com/dbt-labs/dbt-adapters/blob/af33935b119347cc021554ea854884bce986ef8d/dbt-bigquery/src/dbt/adapters/bigquery/impl.py#L98
61
+ "dataset",
62
+ "project",
63
+ "cluster_by",
64
+ "partition_by",
65
+ "kms_key_name",
66
+ "labels",
67
+ "partitions",
68
+ "grant_access_to",
69
+ "hours_to_expiration",
70
+ "require_partition_filter",
71
+ "partition_expiration_days",
72
+ "merge_update_columns",
73
+ "enable_refresh",
74
+ "refresh_interval_minutes",
75
+ "max_staleness",
76
+ "enable_list_inference",
77
+ "intermediate_format",
78
+ "submission_method",
79
+ # Postgres
80
+ "unlogged",
81
+ "indexes",
82
+ # Redshift
83
+ "sort_type",
84
+ "dist",
85
+ "sort",
86
+ "bind",
87
+ "backup",
88
+ "auto_refresh",
89
+ # DBX: https://github.com/databricks/dbt-databricks/blob/fde68588d45e8a299a83318acdca6c97081088e8/dbt/adapters/databricks/impl.py#L105
90
+ "file_format",
91
+ "table_format",
92
+ "location_root",
93
+ "include_full_name_in_path",
94
+ "partition_by",
95
+ "clustered_by",
96
+ "liquid_clustered_by",
97
+ "auto_liquid_cluster",
98
+ "buckets",
99
+ "options",
100
+ "merge_update_columns",
101
+ "merge_exclude_columns",
102
+ "databricks_tags",
103
+ "tblproperties",
104
+ "zorder",
105
+ "unique_tmp_table_suffix",
106
+ "skip_non_matched_step",
107
+ "skip_matched_step",
108
+ "matched_condition",
109
+ "not_matched_condition",
110
+ "not_matched_by_source_action",
111
+ "not_matched_by_source_condition",
112
+ "target_alias",
113
+ "source_alias",
114
+ "merge_with_schema_evolution",
115
+ # moved from property
116
+ "docs",
117
+ "access",
118
+ "group",
119
+ # new
120
+ "freshness",
121
+ ]
122
+ ),
123
+ allowed_properties=set(
124
+ [
125
+ "name",
126
+ "description",
127
+ "latest_version",
128
+ "deprecation_date",
129
+ "config",
130
+ "constraints",
131
+ "data_tests",
132
+ "tests",
133
+ "columns",
134
+ "time_spine",
135
+ "versions",
136
+ ]
137
+ ),
138
+ )
139
+ sources_allowed_config = AllowedConfig(
140
+ allowed_config_fields=set(["enabled", "event_time", "meta", "freshness", "tags"]),
141
+ allowed_properties=set(
142
+ [
143
+ "name",
144
+ "description",
145
+ "database",
146
+ "schema",
147
+ "loader",
148
+ "loaded_at_field",
149
+ "quoting",
150
+ "tables",
151
+ "config",
152
+ ]
153
+ ),
154
+ )
155
+ snapshots_allowed_config = AllowedConfig(
156
+ allowed_config_fields=set(
157
+ [
158
+ # snapshot specific
159
+ "database",
160
+ "schema",
161
+ "unique_key",
162
+ "strategy",
163
+ "updated_at",
164
+ "check_cols",
165
+ "snapshot_meta_column_names",
166
+ "hard_deletes",
167
+ "dbt_valid_to_current",
168
+ # general
169
+ "enabled",
170
+ "tags",
171
+ "alias",
172
+ "pre_hook",
173
+ "post_hook",
174
+ "persist_docs",
175
+ "grants",
176
+ "event_time",
177
+ # moved from property
178
+ "docs",
179
+ # not in docs for config
180
+ "meta",
181
+ ]
182
+ ),
183
+ allowed_properties=set(
184
+ [
185
+ "name",
186
+ "description",
187
+ "config",
188
+ "tests",
189
+ "columns",
190
+ ]
191
+ ),
192
+ )
193
+ seeds_allowed_config = AllowedConfig(
194
+ allowed_config_fields=set(
195
+ [
196
+ # seed specific
197
+ "quote_columns",
198
+ "column_types",
199
+ "delimiter",
200
+ # general
201
+ "enabled",
202
+ "tags",
203
+ "pre_hook",
204
+ "post_hook",
205
+ "database",
206
+ "schema",
207
+ "alias",
208
+ "persist_docs",
209
+ "full_refresh",
210
+ "meta",
211
+ "grants",
212
+ "event_time",
213
+ # moved from property
214
+ "docs",
215
+ ]
216
+ ),
217
+ allowed_properties=set(
218
+ [
219
+ "name",
220
+ "description",
221
+ "config",
222
+ "tests",
223
+ "columns",
224
+ ]
225
+ ),
226
+ )
227
+
228
+ tests_allowed_config = AllowedConfig(
229
+ allowed_config_fields=set(
230
+ [
231
+ # general
232
+ "enabled",
233
+ "tags",
234
+ "meta",
235
+ "database",
236
+ "schema",
237
+ "alias",
238
+ # data tests
239
+ "fail_calc",
240
+ "limit",
241
+ "severity",
242
+ "error_if",
243
+ "warn_if",
244
+ "store_failures",
245
+ "where",
246
+ ]
247
+ ),
248
+ allowed_properties=set(
249
+ [
250
+ "name",
251
+ "description",
252
+ "config",
253
+ ]
254
+ ),
255
+ )
256
+
257
+ fields_per_node_type = {
258
+ "models": models_allowed_config,
259
+ "seeds": seeds_allowed_config,
260
+ "tests": tests_allowed_config,
261
+ "sources": sources_allowed_config,
262
+ "snapshots": snapshots_allowed_config,
263
+ }
264
+
265
+
266
+ def print_matrix(): # noqa: PLR0912
267
+ from dbt_autofix.retrieve_schemas import yaml_specs_per_node_type
268
+
269
+ results = dict()
270
+ for node_type, fields_config in fields_per_node_type.items():
271
+ allowed_config_fields = fields_config.allowed_config_fields
272
+ for field in allowed_config_fields:
273
+ if field not in results:
274
+ results[field] = {f"{node_type}-bestguess": "config"}
275
+ else:
276
+ results[field].update({f"{node_type}-bestguess": "config"})
277
+
278
+ allowed_properties = fields_config.allowed_properties
279
+ for property in allowed_properties:
280
+ if property not in results:
281
+ results[property] = {f"{node_type}-bestguess": "property"}
282
+ else:
283
+ results[property].update({f"{node_type}-bestguess": "property"})
284
+
285
+ for node_type, fields_config in yaml_specs_per_node_type.items():
286
+ allowed_config_fields = fields_config.allowed_config_fields
287
+ for field in allowed_config_fields:
288
+ if field not in results:
289
+ results[field] = {f"{node_type}-fusion": "config"}
290
+ else:
291
+ results[field].update({f"{node_type}-fusion": "config"})
292
+
293
+ allowed_properties = fields_config.allowed_properties
294
+ for property in allowed_properties:
295
+ if property not in results:
296
+ results[property] = {f"{node_type}-fusion": "property"}
297
+ else:
298
+ results[property].update({f"{node_type}-fusion": "property"})
299
+
300
+ # Assuming your dictionary is named 'results'
301
+ with open("config_resources.csv", "w", newline="") as csvfile:
302
+ # Determine all possible resource types from the data
303
+ resource_types = sorted(
304
+ set(resource_type for config_values in results.values() for resource_type in config_values.keys())
305
+ )
306
+
307
+ # Create fieldnames with 'field_name' as the first column followed by all resource types
308
+ fieldnames = ["field_name", *resource_types]
309
+
310
+ # Create CSV writer
311
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
312
+
313
+ # Write the header
314
+ writer.writeheader()
315
+
316
+ # Sort field_names alphabetically
317
+ sorted_field_names = sorted(results.keys())
318
+
319
+ # Write each row
320
+ for field_name in sorted_field_names:
321
+ resources = results[field_name]
322
+ row = {"field_name": field_name}
323
+ # Add values for each resource type
324
+ for resource_type in resource_types:
325
+ row[resource_type] = resources.get(resource_type, "")
326
+ writer.writerow(row)
327
+
328
+ print("CSV file 'config_resources.csv' has been created successfully.")
@@ -0,0 +1,60 @@
1
+ from pathlib import Path
2
+
3
+ import typer
4
+ from rich import print
5
+ from rich.console import Console
6
+ from typing_extensions import Annotated
7
+
8
+ from dbt_autofix.duplicate_keys import find_duplicate_keys, print_duplicate_keys
9
+ from dbt_autofix.fields_properties_configs import print_matrix
10
+ from dbt_autofix.refactor import apply_changesets, changeset_all_sql_yml_files
11
+
12
+ console = Console()
13
+ error_console = Console(stderr=True)
14
+
15
+ app = typer.Typer(
16
+ help="A tool to help clean up dbt projects",
17
+ no_args_is_help=True,
18
+ add_completion=False,
19
+ )
20
+
21
+ current_dir = Path.cwd()
22
+
23
+
24
+ @app.command(name="list-yaml-duplicates")
25
+ def identify_duplicate_keys(
26
+ path: Annotated[Path, typer.Option("--path", "-p", help="The path to the dbt project")] = current_dir,
27
+ ):
28
+ print(f"[green]Identifying duplicates in {path}[/green]\n")
29
+ project_duplicates, package_duplicates = find_duplicate_keys(path)
30
+ print_duplicate_keys(project_duplicates, package_duplicates)
31
+
32
+
33
+ @app.command(name="deprecations")
34
+ def refactor_yml(
35
+ path: Annotated[Path, typer.Option("--path", "-p", help="The path to the dbt project")] = current_dir,
36
+ dry_run: Annotated[bool, typer.Option("--dry-run", "-d", help="In dry run mode, do not apply changes")] = False,
37
+ json_output: Annotated[bool, typer.Option("--json", "-j", help="Output in JSON format")] = False,
38
+ ):
39
+ changesets = changeset_all_sql_yml_files(path, dry_run)
40
+ yaml_results, sql_results = changesets
41
+ if dry_run:
42
+ if not json_output:
43
+ error_console.print("[red]-- Dry run mode, not applying changes --[/red]")
44
+ for changeset in yaml_results:
45
+ if changeset.refactored:
46
+ changeset.print_to_console(json_output)
47
+ for changeset in sql_results:
48
+ if changeset.refactored:
49
+ changeset.print_to_console(json_output)
50
+ else:
51
+ apply_changesets(yaml_results, sql_results, json_output)
52
+
53
+
54
+ @app.command(hidden=True)
55
+ def print_fields_matrix():
56
+ print_matrix()
57
+
58
+
59
+ if __name__ == "__main__":
60
+ app()