datasecops-cli 0.2.1__tar.gz → 0.2.3__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 (47) hide show
  1. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/CHANGELOG.md +25 -0
  2. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/PKG-INFO +1 -1
  3. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/pyproject.toml +1 -1
  4. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/setup.ps1 +48 -6
  5. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/setup.sh +45 -6
  6. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/menus/development.py +9 -8
  7. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/services/download_service.py +137 -23
  8. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/services/linting_service.py +9 -0
  9. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/.github/workflows/publish-cli.yml +0 -0
  10. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/.gitignore +0 -0
  11. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/DEVELOPMENT.md +0 -0
  12. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/LICENSE +0 -0
  13. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/README.md +0 -0
  14. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/docs/getting-started.md +0 -0
  15. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/docs/legacy.md +0 -0
  16. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/docs/legacy_plan_of_action.md +0 -0
  17. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/docs/mcp-server.md +0 -0
  18. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/mcp-servers.json +0 -0
  19. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/__init__.py +0 -0
  20. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/config.py +0 -0
  21. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/main.py +0 -0
  22. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/menus/__init__.py +0 -0
  23. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/menus/downloads.py +0 -0
  24. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/menus/git_operations.py +0 -0
  25. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/models/__init__.py +0 -0
  26. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/models/git_helpers.py +0 -0
  27. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/models/project_config.py +0 -0
  28. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/services/__init__.py +0 -0
  29. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/services/bootstrap_service.py +0 -0
  30. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/services/dbt_runner.py +0 -0
  31. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/services/git_service.py +0 -0
  32. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/services/skill_service.py +0 -0
  33. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/services/snowflake_service.py +0 -0
  34. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/utilities/__init__.py +0 -0
  35. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/utilities/display.py +0 -0
  36. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/utilities/file_utils.py +0 -0
  37. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
  38. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_mcp/__init__.py +0 -0
  39. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_mcp/__main__.py +0 -0
  40. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_mcp/connection.py +0 -0
  41. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/src/datasecops_mcp/server.py +0 -0
  42. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/tests/__init__.py +0 -0
  43. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/tests/test_config.py +0 -0
  44. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/tests/test_file_utils.py +0 -0
  45. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/tests/test_models.py +0 -0
  46. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/tests/test_version.py +0 -0
  47. {datasecops_cli-0.2.1 → datasecops_cli-0.2.3}/tests/test_yaml_utils.py +0 -0
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes to the DataSecOps CLI are documented in this file.
4
4
 
5
+ ## [0.2.3] - 2026-05-11
6
+
7
+ ### Fixed
8
+
9
+ - **Stripped invalid `dbt_skip_compilation_error` option** from the generated `[sqlfluff:templater:dbt]` section — this native app field is not a valid sqlfluff config key and caused parse errors
10
+ - **`profiles_dir = ${DBT_PROFILES_DIR}` no longer leaks** into generated config — the raw value from the native app is now always replaced with the resolved local path or removed entirely
11
+ - **Empty string values** (e.g. `warnings = ""`) now emit `None` instead of blank, matching sqlfluff's expected INI format
12
+
13
+ ### Added
14
+
15
+ - **Auto-install `sqlfluff-templater-dbt`** — the lint menu now checks for missing sqlfluff packages before linting and installs them automatically
16
+
17
+ ## [0.2.2] - 2026-05-10
18
+
19
+ ### Fixed
20
+
21
+ - **Full SQLFluff config generation** — `download_sqlfluff_config()` now generates all sections from the native app config: core, templater, indentation, layout types, global rules, and all rule bundles (aliasing, ambiguous, capitalisation, convention, jinja, layout, references, structure)
22
+ - Previously only `core` and `indentation` sections were emitted, missing ~90% of the config
23
+
24
+ ### Added
25
+
26
+ - **Setup script connection selector** — `setup.ps1` and `setup.sh` now parse `~/.snowflake/connections.toml`, display a numbered list of connections with the default highlighted, and let the user select by number instead of typing the name manually
27
+ - Rule code to section name mapping (`RULE_SECTION_MAP`) for all sqlfluff rule bundles (AL01-AL09, AM01-AM08, CP01-CP05, CV01-CV12, JJ01, LT01-LT15, RF01-RF06, ST01-ST11)
28
+ - Value formatting helper (`_format_value`) for correct INI output of bools, lists, nulls, and empty strings
29
+
5
30
  ## [0.2.1] - 2026-05-10
6
31
 
7
32
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datasecops-cli
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: DataSecOps Framework CLI for Snowflake Native App
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "datasecops-cli"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  description = "DataSecOps Framework CLI for Snowflake Native App"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -48,18 +48,60 @@ Write-Host "--- Snowflake Connection ---" -ForegroundColor Green
48
48
  Write-Host ""
49
49
 
50
50
  $connectionsFile = Join-Path $env:USERPROFILE ".snowflake\connections.toml"
51
+ $connectionNames = @()
52
+ $defaultConnection = $null
53
+
51
54
  if (Test-Path $connectionsFile) {
55
+ # Parse default connection name
56
+ $defaultLine = Get-Content $connectionsFile | Select-String '^default_connection_name\s*=' | Select-Object -First 1
57
+ if ($defaultLine) {
58
+ $defaultConnection = ($defaultLine -replace '.*=\s*', '').Trim().Trim('"').Trim("'")
59
+ }
60
+
61
+ # Parse connection section names
62
+ $connectionNames = @(Get-Content $connectionsFile | Select-String '^\[([^\]]+)\]' | ForEach-Object { $_.Matches[0].Groups[1].Value })
63
+ }
64
+
65
+ if ($connectionNames.Count -gt 0) {
52
66
  Write-Host "Available connections:" -ForegroundColor Cyan
53
- Get-Content $connectionsFile | Select-String '^\[' | ForEach-Object { $_.Line -replace '[\[\]]', '' }
67
+ for ($i = 0; $i -lt $connectionNames.Count; $i++) {
68
+ $marker = ""
69
+ if ($connectionNames[$i] -eq $defaultConnection) { $marker = " (default)" }
70
+ Write-Host " [$($i + 1)] $($connectionNames[$i])$marker"
71
+ }
54
72
  Write-Host ""
55
- }
56
73
 
57
- $connectionName = Read-Host "Enter connection name"
58
- if ([string]::IsNullOrWhiteSpace($connectionName)) {
59
- Write-Host "ERROR: Connection name is required" -ForegroundColor Red
60
- exit 1
74
+ $defaultIndex = -1
75
+ if ($defaultConnection) {
76
+ $defaultIndex = [array]::IndexOf($connectionNames, $defaultConnection)
77
+ }
78
+
79
+ if ($defaultIndex -ge 0) {
80
+ $prompt = "Select connection [1-$($connectionNames.Count)] (default: $($defaultIndex + 1))"
81
+ } else {
82
+ $prompt = "Select connection [1-$($connectionNames.Count)]"
83
+ }
84
+
85
+ $selection = Read-Host $prompt
86
+ if ([string]::IsNullOrWhiteSpace($selection) -and $defaultIndex -ge 0) {
87
+ $connectionName = $defaultConnection
88
+ } elseif ($selection -match '^\d+$' -and [int]$selection -ge 1 -and [int]$selection -le $connectionNames.Count) {
89
+ $connectionName = $connectionNames[[int]$selection - 1]
90
+ } else {
91
+ Write-Host "ERROR: Invalid selection" -ForegroundColor Red
92
+ exit 1
93
+ }
94
+ } else {
95
+ Write-Host "No connections found in $connectionsFile" -ForegroundColor Yellow
96
+ $connectionName = Read-Host "Enter connection name"
97
+ if ([string]::IsNullOrWhiteSpace($connectionName)) {
98
+ Write-Host "ERROR: Connection name is required" -ForegroundColor Red
99
+ exit 1
100
+ }
61
101
  }
62
102
 
103
+ Write-Host "[OK] Using connection: $connectionName" -ForegroundColor Green
104
+
63
105
  # --- Step 3: Enter native app database name ---
64
106
 
65
107
  Write-Host ""
@@ -46,18 +46,57 @@ echo "--- Snowflake Connection ---"
46
46
  echo ""
47
47
 
48
48
  CONNECTIONS_FILE="$HOME/.snowflake/connections.toml"
49
+ CONNECTION_NAMES=()
50
+ DEFAULT_CONNECTION=""
51
+
49
52
  if [ -f "$CONNECTIONS_FILE" ]; then
53
+ # Parse default connection name
54
+ DEFAULT_CONNECTION=$(grep '^default_connection_name' "$CONNECTIONS_FILE" | sed 's/.*=\s*//;s/[" ]//g' | head -1)
55
+
56
+ # Parse connection section names
57
+ while IFS= read -r name; do
58
+ CONNECTION_NAMES+=("$name")
59
+ done < <(grep '^\[' "$CONNECTIONS_FILE" | sed 's/\[//;s/\]//')
60
+ fi
61
+
62
+ if [ ${#CONNECTION_NAMES[@]} -gt 0 ]; then
50
63
  echo "Available connections:"
51
- grep '^\[' "$CONNECTIONS_FILE" | sed 's/\[//;s/\]//' | nl -ba
64
+ DEFAULT_INDEX=-1
65
+ for i in "${!CONNECTION_NAMES[@]}"; do
66
+ marker=""
67
+ if [ "${CONNECTION_NAMES[$i]}" = "$DEFAULT_CONNECTION" ]; then
68
+ marker=" (default)"
69
+ DEFAULT_INDEX=$i
70
+ fi
71
+ echo " [$((i + 1))] ${CONNECTION_NAMES[$i]}${marker}"
72
+ done
52
73
  echo ""
53
- fi
54
74
 
55
- read -rp "Enter connection name: " CONNECTION_NAME
56
- if [ -z "$CONNECTION_NAME" ]; then
57
- echo "ERROR: Connection name is required"
58
- exit 1
75
+ if [ $DEFAULT_INDEX -ge 0 ]; then
76
+ read -rp "Select connection [1-${#CONNECTION_NAMES[@]}] (default: $((DEFAULT_INDEX + 1))): " SELECTION
77
+ else
78
+ read -rp "Select connection [1-${#CONNECTION_NAMES[@]}]: " SELECTION
79
+ fi
80
+
81
+ if [ -z "$SELECTION" ] && [ $DEFAULT_INDEX -ge 0 ]; then
82
+ CONNECTION_NAME="$DEFAULT_CONNECTION"
83
+ elif [[ "$SELECTION" =~ ^[0-9]+$ ]] && [ "$SELECTION" -ge 1 ] && [ "$SELECTION" -le ${#CONNECTION_NAMES[@]} ]; then
84
+ CONNECTION_NAME="${CONNECTION_NAMES[$((SELECTION - 1))]}"
85
+ else
86
+ echo "ERROR: Invalid selection"
87
+ exit 1
88
+ fi
89
+ else
90
+ echo "No connections found in $CONNECTIONS_FILE"
91
+ read -rp "Enter connection name: " CONNECTION_NAME
92
+ if [ -z "$CONNECTION_NAME" ]; then
93
+ echo "ERROR: Connection name is required"
94
+ exit 1
95
+ fi
59
96
  fi
60
97
 
98
+ echo "[OK] Using connection: $CONNECTION_NAME"
99
+
61
100
  # --- Step 3: Enter native app database name ---
62
101
 
63
102
  echo ""
@@ -141,15 +141,16 @@ class DevelopmentMenu:
141
141
  complete_action()
142
142
 
143
143
  def _ensure_sqlfluff_config(self) -> bool:
144
- """Download .sqlfluff from the framework if it doesn't exist locally."""
144
+ """Download .sqlfluff from the framework if it doesn't exist locally, and ensure templater is installed."""
145
145
  config_path = self.linting.project_dir / ".sqlfluff"
146
- if config_path.exists():
147
- return True
148
- if not self.downloads:
149
- error_line("No .sqlfluff config found and download service not available")
150
- return False
151
- info_line("No .sqlfluff config found — downloading from framework...")
152
- return self.downloads.download_sqlfluff_config(profiles_dir=self.profiles_dir)
146
+ if not config_path.exists():
147
+ if not self.downloads:
148
+ error_line("No .sqlfluff config found and download service not available")
149
+ return False
150
+ info_line("No .sqlfluff config found — downloading from framework...")
151
+ if not self.downloads.download_sqlfluff_config(profiles_dir=self.profiles_dir):
152
+ return False
153
+ return self.linting.ensure_templater_installed()
153
154
 
154
155
  def _lint_menu(self) -> None:
155
156
  clear()
@@ -12,10 +12,88 @@ from datasecops_cli.utilities.file_utils import write_file, ensure_dir
12
12
 
13
13
  class DownloadService:
14
14
  """Downloads configurations from the native app."""
15
+
16
+ # Maps rule codes to their sqlfluff INI section names.
17
+ RULE_SECTION_MAP: dict[str, str] = {
18
+ # Aliasing
19
+ "AL01": "aliasing.table", "AL02": "aliasing.column", "AL03": "aliasing.expression",
20
+ "AL04": "aliasing.unique.table", "AL05": "aliasing.unused", "AL06": "aliasing.length",
21
+ "AL07": "aliasing.forbid", "AL08": "aliasing.unique.column", "AL09": "aliasing.self_alias",
22
+ # Ambiguous
23
+ "AM01": "ambiguous.distinct", "AM02": "ambiguous.union", "AM03": "ambiguous.order_by",
24
+ "AM04": "ambiguous.column_count", "AM05": "ambiguous.join",
25
+ "AM06": "ambiguous.column_references", "AM07": "ambiguous.set_columns",
26
+ "AM08": "ambiguous.union_type",
27
+ # Capitalisation
28
+ "CP01": "capitalisation.keywords", "CP02": "capitalisation.identifiers",
29
+ "CP03": "capitalisation.functions", "CP04": "capitalisation.literals",
30
+ "CP05": "capitalisation.types",
31
+ # Convention
32
+ "CV01": "convention.not_equal", "CV02": "convention.coalesce",
33
+ "CV03": "convention.select_trailing_comma", "CV04": "convention.count_rows",
34
+ "CV05": "convention.is_null", "CV06": "convention.terminator",
35
+ "CV07": "convention.statement_brackets", "CV08": "convention.left_join",
36
+ "CV09": "convention.blocked_words", "CV10": "convention.quoted_literals",
37
+ "CV11": "convention.casting_style", "CV12": "convention.semicolon",
38
+ # Jinja
39
+ "JJ01": "jinja.padding",
40
+ # Layout
41
+ "LT01": "layout.spacing", "LT02": "layout.indent", "LT03": "layout.operators",
42
+ "LT04": "layout.commas", "LT05": "layout.long_lines", "LT06": "layout.functions",
43
+ "LT07": "layout.cte_bracket", "LT08": "layout.cte_newline",
44
+ "LT09": "layout.select_targets", "LT10": "layout.select_modifiers",
45
+ "LT11": "layout.set_operators", "LT12": "layout.end-of-file",
46
+ "LT13": "layout.start_of_file", "LT14": "layout.one_line", "LT15": "layout.newlines",
47
+ # References
48
+ "RF01": "references.from", "RF02": "references.qualification",
49
+ "RF03": "references.consistent", "RF04": "references.keywords",
50
+ "RF05": "references.special_chars", "RF06": "references.quoting",
51
+ # Structure
52
+ "ST01": "structure.else_null", "ST02": "structure.simple_case",
53
+ "ST03": "structure.unused_cte", "ST04": "structure.nested_case",
54
+ "ST05": "structure.subquery", "ST06": "structure.column_order",
55
+ "ST07": "structure.using", "ST08": "structure.distinct",
56
+ "ST09": "structure.join_condition_order", "ST10": "structure.cte_definition_order",
57
+ "ST11": "structure.test_query",
58
+ }
59
+
60
+ # Core sub-keys that map to specific INI sections.
61
+ CORE_SECTION_MAP: dict[str, str] = {
62
+ "sqlfluff": "sqlfluff",
63
+ "dbt": "sqlfluff:templater:dbt",
64
+ "jinja": "sqlfluff:templater:jinja",
65
+ "templater": "sqlfluff:templater",
66
+ }
15
67
 
16
68
  def __init__(self, snowflake_service: SnowflakeService, project_dir: Path):
17
69
  self.sf = snowflake_service
18
70
  self.project_dir = project_dir
71
+
72
+ # Options to strip from specific core sections (not valid sqlfluff config keys).
73
+ CORE_STRIP_OPTIONS: dict[str, set[str]] = {
74
+ "dbt": {"dbt_skip_compilation_error"},
75
+ }
76
+
77
+ @staticmethod
78
+ def _format_value(val) -> str:
79
+ """Format a JSON value for sqlfluff INI output."""
80
+ if isinstance(val, bool):
81
+ return str(val)
82
+ if isinstance(val, list):
83
+ if not val:
84
+ return "None"
85
+ return ", ".join(str(v) for v in val)
86
+ if val is None or val == "":
87
+ return "None"
88
+ return str(val)
89
+
90
+ @staticmethod
91
+ def _emit_section(lines: list[str], header: str, options: dict) -> None:
92
+ """Append an INI section with its options to lines."""
93
+ lines.append(f"[{header}]")
94
+ for key, val in options.items():
95
+ lines.append(f"{key} = {DownloadService._format_value(val)}")
96
+ lines.append("")
19
97
 
20
98
  def download_sqlfluff_config(self, profiles_dir: str = None) -> bool:
21
99
  info_line("Downloading SQLFluff configuration...")
@@ -24,31 +102,67 @@ class DownloadService:
24
102
  error_line("No SQLFluff configuration found in native app")
25
103
  return False
26
104
 
27
- lines = ["[sqlfluff]", "dialect = snowflake", "templater = dbt", ""]
28
-
105
+ lines: list[str] = []
106
+
107
+ # --- Core sections ([sqlfluff], [sqlfluff:templater:dbt], etc.) ---
29
108
  core = raw.get("core", {})
30
- for code, state in core.items():
31
- if isinstance(state, dict) and state.get("enabled", True):
32
- opts = state.get("options", {})
33
- for opt_key, opt_val in opts.items():
34
- lines.append(f"{opt_key} = {opt_val}")
35
-
36
- lines.append("")
37
- lines.append("[sqlfluff:templater:dbt]")
38
- lines.append("project_dir = .")
39
- if profiles_dir:
40
- lines.append(f"profiles_dir = {profiles_dir}")
41
-
42
- lines.append("")
43
- lines.append("[sqlfluff:indentation]")
109
+ for key, section_header in self.CORE_SECTION_MAP.items():
110
+ entry = core.get(key)
111
+ if not isinstance(entry, dict) or not entry.get("enabled", True):
112
+ continue
113
+ opts = dict(entry.get("options", {}))
114
+ # Strip options that are not valid sqlfluff config keys
115
+ for strip_key in self.CORE_STRIP_OPTIONS.get(key, set()):
116
+ opts.pop(strip_key, None)
117
+ if key == "dbt":
118
+ # Override profiles_dir / project_dir with local values
119
+ opts["project_dir"] = "."
120
+ if profiles_dir:
121
+ opts["profiles_dir"] = profiles_dir
122
+ else:
123
+ opts.pop("profiles_dir", None)
124
+ if opts:
125
+ self._emit_section(lines, section_header, opts)
126
+
127
+ # --- Indentation ---
44
128
  indentation = raw.get("indentation", {})
45
- for code, state in indentation.items():
46
- if isinstance(state, dict):
47
- opts = state.get("options", {})
48
- for opt_key, opt_val in opts.items():
49
- lines.append(f"{opt_key} = {opt_val}")
50
-
51
- content = "\n".join(lines) + "\n"
129
+ for _code, entry in indentation.items():
130
+ if isinstance(entry, dict) and entry.get("enabled", True):
131
+ opts = entry.get("options", {})
132
+ if opts:
133
+ self._emit_section(lines, "sqlfluff:indentation", opts)
134
+
135
+ # --- Layout sections ([sqlfluff:layout:type:<code>]) ---
136
+ layout = raw.get("layout", {})
137
+ for code, entry in layout.items():
138
+ if not isinstance(entry, dict) or not entry.get("enabled", True):
139
+ continue
140
+ opts = entry.get("options", {})
141
+ if opts:
142
+ self._emit_section(lines, f"sqlfluff:layout:type:{code}", opts)
143
+
144
+ # --- Global rules ([sqlfluff:rules]) ---
145
+ rules = raw.get("rules", {})
146
+ for _code, entry in rules.items():
147
+ if isinstance(entry, dict) and entry.get("enabled", True):
148
+ opts = entry.get("options", {})
149
+ if opts:
150
+ self._emit_section(lines, "sqlfluff:rules", opts)
151
+
152
+ # --- Rule bundle sections (rules_aliasing, rules_capitalisation, etc.) ---
153
+ for section_key, entries in raw.items():
154
+ if not section_key.startswith("rules_") or not isinstance(entries, dict):
155
+ continue
156
+ for code, entry in entries.items():
157
+ if not isinstance(entry, dict) or not entry.get("enabled", True):
158
+ continue
159
+ section_name = self.RULE_SECTION_MAP.get(code)
160
+ if not section_name:
161
+ continue
162
+ opts = entry.get("options", {})
163
+ self._emit_section(lines, f"sqlfluff:rules:{section_name}", opts)
164
+
165
+ content = "\n".join(lines)
52
166
  dest = self.project_dir / ".sqlfluff"
53
167
  write_file(dest, content)
54
168
  success_line(f"SQLFluff config written to {dest}")
@@ -43,6 +43,15 @@ class LintingService:
43
43
  error_line(f"pip install failed with exit code {result.returncode}")
44
44
  return False
45
45
 
46
+ def ensure_templater_installed(self) -> bool:
47
+ """Check if sqlfluff-templater-dbt is installed; install it if missing."""
48
+ versions = self.get_installed_versions()
49
+ missing = [pkg for pkg in ["sqlfluff", "sqlfluff-templater-dbt"] if not versions.get(pkg)]
50
+ if not missing:
51
+ return True
52
+ warning_line(f"Missing packages: {', '.join(missing)}")
53
+ return self.install_requirements(missing)
54
+
46
55
  def lint_file(self, file_path: str = None, fix: bool = False) -> subprocess.CompletedProcess:
47
56
  action = "fix" if fix else "lint"
48
57
  cmd = ["sqlfluff", action]
File without changes
File without changes