datasecops-cli 0.2.0__tar.gz → 0.2.2__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.
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/CHANGELOG.md +27 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/PKG-INFO +1 -1
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/pyproject.toml +1 -1
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/setup.ps1 +48 -6
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/setup.sh +45 -6
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/main.py +3 -2
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/menus/development.py +19 -1
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/menus/downloads.py +2 -1
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/services/bootstrap_service.py +2 -1
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/services/download_service.py +130 -18
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/.github/workflows/publish-cli.yml +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/.gitignore +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/DEVELOPMENT.md +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/LICENSE +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/README.md +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/docs/getting-started.md +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/docs/legacy.md +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/docs/legacy_plan_of_action.md +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/docs/mcp-server.md +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/mcp-servers.json +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/__init__.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/config.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/menus/__init__.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/menus/git_operations.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/models/__init__.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/models/git_helpers.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/models/project_config.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/services/__init__.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/services/dbt_runner.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/services/git_service.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/services/linting_service.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/services/skill_service.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/services/snowflake_service.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/utilities/__init__.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/utilities/display.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/utilities/file_utils.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_mcp/__init__.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_mcp/__main__.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_mcp/connection.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_mcp/server.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/tests/__init__.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/tests/test_config.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/tests/test_file_utils.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/tests/test_models.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/tests/test_version.py +0 -0
- {datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/tests/test_yaml_utils.py +0 -0
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the DataSecOps CLI are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.2] - 2026-05-10
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **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)
|
|
10
|
+
- Previously only `core` and `indentation` sections were emitted, missing ~90% of the config
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **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
|
|
15
|
+
- 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)
|
|
16
|
+
- Value formatting helper (`_format_value`) for correct INI output of bools, lists, nulls, and empty strings
|
|
17
|
+
|
|
18
|
+
## [0.2.1] - 2026-05-10
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **SQLFluff config now includes `[sqlfluff:templater:dbt]` section** with `project_dir` and `profiles_dir` when downloaded from the framework, eliminating the need for `${DBT_PROFILES_DIR}` environment variable substitution
|
|
23
|
+
- **Auto-download `.sqlfluff` before linting** — the lint menu now automatically downloads the config from the framework if it doesn't exist locally, instead of failing silently or erroring
|
|
24
|
+
- **CI lint action handles missing `.sqlfluffconfig`** — the `tasks/lint-sql` action now only runs `envsubst` if `.sqlfluffconfig` exists, and accepts a pre-generated `.sqlfluff` file directly
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `download_sqlfluff_config()` accepts an optional `profiles_dir` parameter to embed in the generated config
|
|
29
|
+
- `DevelopmentMenu` and `DownloadsMenu` pass the resolved `profiles_dir` through to the download service
|
|
30
|
+
- `.sqlfluff` no longer needs to be committed to client repos — it is generated on-the-fly from the framework
|
|
31
|
+
|
|
5
32
|
## [0.2.0] - 2026-05-09
|
|
6
33
|
|
|
7
34
|
### Added
|
|
@@ -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
|
-
|
|
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
|
-
$
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 ""
|
|
@@ -88,12 +88,13 @@ def _main_menu(config: Config, dbt_runner: DbtRunner, git_service: GitService,
|
|
|
88
88
|
|
|
89
89
|
while option != 0:
|
|
90
90
|
if option == 1:
|
|
91
|
+
profiles_dir = str(config.get_dbt_profiles_dir())
|
|
91
92
|
if git_service:
|
|
92
93
|
dev_menu = DevelopmentMenu(dbt_runner, linting_service, git_service, profile_name,
|
|
93
|
-
download_service=download_service)
|
|
94
|
+
download_service=download_service, profiles_dir=profiles_dir)
|
|
94
95
|
else:
|
|
95
96
|
dev_menu = DevelopmentMenu(dbt_runner, linting_service, None, profile_name,
|
|
96
|
-
download_service=download_service)
|
|
97
|
+
download_service=download_service, profiles_dir=profiles_dir)
|
|
97
98
|
dev_menu.show()
|
|
98
99
|
|
|
99
100
|
elif option == 2 and git_service:
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
1
3
|
from datasecops_cli.services.dbt_runner import DbtRunner
|
|
2
4
|
from datasecops_cli.services.linting_service import LintingService
|
|
3
5
|
from datasecops_cli.services.download_service import DownloadService
|
|
@@ -12,12 +14,14 @@ from datasecops_cli.utilities.display import (
|
|
|
12
14
|
class DevelopmentMenu:
|
|
13
15
|
def __init__(self, dbt_runner: DbtRunner, linting_service: LintingService,
|
|
14
16
|
git_service: GitService, profile_name: str,
|
|
15
|
-
download_service: DownloadService = None
|
|
17
|
+
download_service: DownloadService = None,
|
|
18
|
+
profiles_dir: str = None):
|
|
16
19
|
self.dbt = dbt_runner
|
|
17
20
|
self.linting = linting_service
|
|
18
21
|
self.git = git_service
|
|
19
22
|
self.profile_name = profile_name
|
|
20
23
|
self.downloads = download_service
|
|
24
|
+
self.profiles_dir = profiles_dir
|
|
21
25
|
|
|
22
26
|
def show(self) -> None:
|
|
23
27
|
self._menu()
|
|
@@ -136,6 +140,17 @@ class DevelopmentMenu:
|
|
|
136
140
|
self.dbt.test(select=select)
|
|
137
141
|
complete_action()
|
|
138
142
|
|
|
143
|
+
def _ensure_sqlfluff_config(self) -> bool:
|
|
144
|
+
"""Download .sqlfluff from the framework if it doesn't exist locally."""
|
|
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)
|
|
153
|
+
|
|
139
154
|
def _lint_menu(self) -> None:
|
|
140
155
|
clear()
|
|
141
156
|
display_action_header("SQLFluff Linting Options")
|
|
@@ -147,6 +162,9 @@ class DevelopmentMenu:
|
|
|
147
162
|
menu_option(6, "install - Install SQLFluff requirements from framework")
|
|
148
163
|
menu_option(0, "back - Return to development menu")
|
|
149
164
|
option = get_input_number("Choose an option: ")
|
|
165
|
+
if option in (1, 2, 3, 4, 5) and not self._ensure_sqlfluff_config():
|
|
166
|
+
complete_action()
|
|
167
|
+
return
|
|
150
168
|
if option == 1:
|
|
151
169
|
changed = [f.file for f in self.git.get_changed_files()] if self.git else []
|
|
152
170
|
self.linting.lint_modified(fix=False, changed_files=changed)
|
|
@@ -29,7 +29,8 @@ class DownloadsMenu:
|
|
|
29
29
|
while option != 0:
|
|
30
30
|
if option == 1:
|
|
31
31
|
display_action_header("Download SQLFluff Config")
|
|
32
|
-
self.
|
|
32
|
+
profiles_dir = str(Path(self.project_settings.profile_dir).expanduser()) if self.project_settings else None
|
|
33
|
+
self.downloads.download_sqlfluff_config(profiles_dir=profiles_dir)
|
|
33
34
|
complete_action()
|
|
34
35
|
elif option == 2:
|
|
35
36
|
display_action_header("Download Pipeline Files")
|
{datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/services/bootstrap_service.py
RENAMED
|
@@ -68,7 +68,8 @@ class BootstrapService:
|
|
|
68
68
|
# Step 3: Download SQLFluff config
|
|
69
69
|
info_line("")
|
|
70
70
|
info_line("[3] Downloading SQLFluff configuration...")
|
|
71
|
-
|
|
71
|
+
profiles_dir = str(Path(self.project_settings.profile_dir).expanduser())
|
|
72
|
+
if self.download_service.download_sqlfluff_config(profiles_dir=profiles_dir):
|
|
72
73
|
steps_passed += 1
|
|
73
74
|
else:
|
|
74
75
|
info_line(" (skipped - no SQLFluff config in native app)")
|
{datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/services/download_service.py
RENAMED
|
@@ -12,37 +12,149 @@ 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
|
+
@staticmethod
|
|
73
|
+
def _format_value(val) -> str:
|
|
74
|
+
"""Format a JSON value for sqlfluff INI output."""
|
|
75
|
+
if isinstance(val, bool):
|
|
76
|
+
return str(val)
|
|
77
|
+
if isinstance(val, list):
|
|
78
|
+
if not val:
|
|
79
|
+
return "None"
|
|
80
|
+
return ", ".join(str(v) for v in val)
|
|
81
|
+
if val is None or val == "":
|
|
82
|
+
return "None"
|
|
83
|
+
return str(val)
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def _emit_section(lines: list[str], header: str, options: dict) -> None:
|
|
87
|
+
"""Append an INI section with its options to lines."""
|
|
88
|
+
lines.append(f"[{header}]")
|
|
89
|
+
for key, val in options.items():
|
|
90
|
+
lines.append(f"{key} = {DownloadService._format_value(val)}")
|
|
91
|
+
lines.append("")
|
|
19
92
|
|
|
20
|
-
def download_sqlfluff_config(self) -> bool:
|
|
93
|
+
def download_sqlfluff_config(self, profiles_dir: str = None) -> bool:
|
|
21
94
|
info_line("Downloading SQLFluff configuration...")
|
|
22
95
|
raw = self.sf.get_framework_config("SQLFLUFF_RULES")
|
|
23
96
|
if not raw:
|
|
24
97
|
error_line("No SQLFluff configuration found in native app")
|
|
25
98
|
return False
|
|
26
99
|
|
|
27
|
-
lines
|
|
28
|
-
|
|
100
|
+
lines: list[str] = []
|
|
101
|
+
|
|
102
|
+
# --- Core sections ([sqlfluff], [sqlfluff:templater:dbt], etc.) ---
|
|
29
103
|
core = raw.get("core", {})
|
|
30
|
-
for
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
104
|
+
for key, section_header in self.CORE_SECTION_MAP.items():
|
|
105
|
+
entry = core.get(key)
|
|
106
|
+
if not isinstance(entry, dict) or not entry.get("enabled", True):
|
|
107
|
+
continue
|
|
108
|
+
opts = dict(entry.get("options", {}))
|
|
109
|
+
if key == "dbt":
|
|
110
|
+
# Override profiles_dir / project_dir with local values
|
|
111
|
+
opts["project_dir"] = "."
|
|
112
|
+
if profiles_dir:
|
|
113
|
+
opts["profiles_dir"] = profiles_dir
|
|
114
|
+
elif "profiles_dir" in opts:
|
|
115
|
+
del opts["profiles_dir"]
|
|
116
|
+
if opts:
|
|
117
|
+
self._emit_section(lines, section_header, opts)
|
|
118
|
+
|
|
119
|
+
# --- Indentation ---
|
|
38
120
|
indentation = raw.get("indentation", {})
|
|
39
|
-
for
|
|
40
|
-
if isinstance(
|
|
41
|
-
opts =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
121
|
+
for _code, entry in indentation.items():
|
|
122
|
+
if isinstance(entry, dict) and entry.get("enabled", True):
|
|
123
|
+
opts = entry.get("options", {})
|
|
124
|
+
if opts:
|
|
125
|
+
self._emit_section(lines, "sqlfluff:indentation", opts)
|
|
126
|
+
|
|
127
|
+
# --- Layout sections ([sqlfluff:layout:type:<code>]) ---
|
|
128
|
+
layout = raw.get("layout", {})
|
|
129
|
+
for code, entry in layout.items():
|
|
130
|
+
if not isinstance(entry, dict) or not entry.get("enabled", True):
|
|
131
|
+
continue
|
|
132
|
+
opts = entry.get("options", {})
|
|
133
|
+
if opts:
|
|
134
|
+
self._emit_section(lines, f"sqlfluff:layout:type:{code}", opts)
|
|
135
|
+
|
|
136
|
+
# --- Global rules ([sqlfluff:rules]) ---
|
|
137
|
+
rules = raw.get("rules", {})
|
|
138
|
+
for _code, entry in rules.items():
|
|
139
|
+
if isinstance(entry, dict) and entry.get("enabled", True):
|
|
140
|
+
opts = entry.get("options", {})
|
|
141
|
+
if opts:
|
|
142
|
+
self._emit_section(lines, "sqlfluff:rules", opts)
|
|
143
|
+
|
|
144
|
+
# --- Rule bundle sections (rules_aliasing, rules_capitalisation, etc.) ---
|
|
145
|
+
for section_key, entries in raw.items():
|
|
146
|
+
if not section_key.startswith("rules_") or not isinstance(entries, dict):
|
|
147
|
+
continue
|
|
148
|
+
for code, entry in entries.items():
|
|
149
|
+
if not isinstance(entry, dict) or not entry.get("enabled", True):
|
|
150
|
+
continue
|
|
151
|
+
section_name = self.RULE_SECTION_MAP.get(code)
|
|
152
|
+
if not section_name:
|
|
153
|
+
continue
|
|
154
|
+
opts = entry.get("options", {})
|
|
155
|
+
self._emit_section(lines, f"sqlfluff:rules:{section_name}", opts)
|
|
156
|
+
|
|
157
|
+
content = "\n".join(lines)
|
|
46
158
|
dest = self.project_dir / ".sqlfluff"
|
|
47
159
|
write_file(dest, content)
|
|
48
160
|
success_line(f"SQLFluff config written to {dest}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/services/linting_service.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.2.0 → datasecops_cli-0.2.2}/src/datasecops_cli/services/snowflake_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|