datasecops-cli 0.4.5__tar.gz → 0.4.6__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.4.5 → datasecops_cli-0.4.6}/CHANGELOG.md +11 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/PKG-INFO +1 -1
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/pyproject.toml +1 -1
- datasecops_cli-0.4.6/src/datasecops_cli/__init__.py +1 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/menus/development.py +6 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/download_service.py +19 -145
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/snowflake_service.py +5 -0
- datasecops_cli-0.4.5/src/datasecops_cli/__init__.py +0 -1
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/.github/workflows/auto-tag.yml +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/.github/workflows/publish-cli.yml +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/.gitignore +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/DEVELOPMENT.md +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/LICENSE +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/README.md +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/docs/getting-started.md +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/docs/legacy.md +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/docs/legacy_plan_of_action.md +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/docs/mcp-server.md +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/mcp-servers.json +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/setup.ps1 +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/setup.sh +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/config.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/main.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/menus/__init__.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/menus/configuration.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/menus/downloads.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/menus/git_operations.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/models/__init__.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/models/git_helpers.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/models/project_config.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/__init__.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/bootstrap_service.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/dbt_project_generator.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/dbt_runner.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/directory_scaffolder.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/git_service.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/linting_service.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/skill_service.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/upstream_service.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/utilities/__init__.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/utilities/display.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/utilities/file_utils.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_mcp/__init__.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_mcp/__main__.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_mcp/connection.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_mcp/server.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/tests/__init__.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/tests/test_config.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/tests/test_file_utils.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/tests/test_main.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/tests/test_models.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/tests/test_version.py +0 -0
- {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/tests/test_yaml_utils.py +0 -0
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the DataSecOps CLI are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.6] - 2026-05-18
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **SQLFluff config now fetched as rendered INI from native app** — `download_sqlfluff_config()` calls the new `api.get_sqlfluff_config_file()` stored procedure which returns a ready-to-use `.sqlfluff` file, matching exactly what the native app UI displays. Removes client-side INI rendering logic.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Re-download .sqlfluff option in lint menu** — new option `[6] refresh config` in the SQLFluff linting menu to re-download the `.sqlfluff` configuration from the framework at any time, even if the file already exists locally.
|
|
14
|
+
- **`get_sqlfluff_config_file()` method** added to `SnowflakeService` for calling the new native app procedure.
|
|
15
|
+
|
|
5
16
|
## [0.4.5] - 2026-05-18
|
|
6
17
|
|
|
7
18
|
### Fixed
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.6"
|
|
@@ -176,6 +176,7 @@ class DevelopmentMenu:
|
|
|
176
176
|
menu_option(3, "find all - Lint all SQL files")
|
|
177
177
|
menu_option(4, "fix all - Fix all SQL files")
|
|
178
178
|
menu_option(5, "specific - Lint specific file")
|
|
179
|
+
menu_option(6, "refresh config- Re-download .sqlfluff from framework")
|
|
179
180
|
menu_option(0, "back - Return to development menu")
|
|
180
181
|
option = get_input_number("Choose an option: ")
|
|
181
182
|
if option in (1, 2, 3, 4, 5) and not self._ensure_sqlfluff_config():
|
|
@@ -195,6 +196,11 @@ class DevelopmentMenu:
|
|
|
195
196
|
path = get_input_string("Enter SQL file path (e.g. models/staging/stg_orders.sql): ")
|
|
196
197
|
if path != "0":
|
|
197
198
|
self.linting.lint_file(file_path=path, fix=False)
|
|
199
|
+
elif option == 6:
|
|
200
|
+
if not self.downloads:
|
|
201
|
+
error_line("Download service not available")
|
|
202
|
+
else:
|
|
203
|
+
self.downloads.download_sqlfluff_config(profiles_dir=self.profiles_dir, dbt_project_dir=self.linting.project_dir)
|
|
198
204
|
complete_action()
|
|
199
205
|
|
|
200
206
|
def _autofix_menu(self) -> None:
|
{datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/download_service.py
RENAMED
|
@@ -37,161 +37,35 @@ def _collect_upstream_packages(
|
|
|
37
37
|
class DownloadService:
|
|
38
38
|
"""Downloads configurations from the native app."""
|
|
39
39
|
|
|
40
|
-
# Maps rule codes to their sqlfluff INI section names.
|
|
41
|
-
RULE_SECTION_MAP: dict[str, str] = {
|
|
42
|
-
# Aliasing
|
|
43
|
-
"AL01": "aliasing.table", "AL02": "aliasing.column", "AL03": "aliasing.expression",
|
|
44
|
-
"AL04": "aliasing.unique.table", "AL05": "aliasing.unused", "AL06": "aliasing.length",
|
|
45
|
-
"AL07": "aliasing.forbid", "AL08": "aliasing.unique.column", "AL09": "aliasing.self_alias",
|
|
46
|
-
# Ambiguous
|
|
47
|
-
"AM01": "ambiguous.distinct", "AM02": "ambiguous.union", "AM03": "ambiguous.order_by",
|
|
48
|
-
"AM04": "ambiguous.column_count", "AM05": "ambiguous.join",
|
|
49
|
-
"AM06": "ambiguous.column_references", "AM07": "ambiguous.set_columns",
|
|
50
|
-
"AM08": "ambiguous.union_type",
|
|
51
|
-
# Capitalisation
|
|
52
|
-
"CP01": "capitalisation.keywords", "CP02": "capitalisation.identifiers",
|
|
53
|
-
"CP03": "capitalisation.functions", "CP04": "capitalisation.literals",
|
|
54
|
-
"CP05": "capitalisation.types",
|
|
55
|
-
# Convention
|
|
56
|
-
"CV01": "convention.not_equal", "CV02": "convention.coalesce",
|
|
57
|
-
"CV03": "convention.select_trailing_comma", "CV04": "convention.count_rows",
|
|
58
|
-
"CV05": "convention.is_null", "CV06": "convention.terminator",
|
|
59
|
-
"CV07": "convention.statement_brackets", "CV08": "convention.left_join",
|
|
60
|
-
"CV09": "convention.blocked_words", "CV10": "convention.quoted_literals",
|
|
61
|
-
"CV11": "convention.casting_style", "CV12": "convention.semicolon",
|
|
62
|
-
# Jinja
|
|
63
|
-
"JJ01": "jinja.padding",
|
|
64
|
-
# Layout
|
|
65
|
-
"LT01": "layout.spacing", "LT02": "layout.indent", "LT03": "layout.operators",
|
|
66
|
-
"LT04": "layout.commas", "LT05": "layout.long_lines", "LT06": "layout.functions",
|
|
67
|
-
"LT07": "layout.cte_bracket", "LT08": "layout.cte_newline",
|
|
68
|
-
"LT09": "layout.select_targets", "LT10": "layout.select_modifiers",
|
|
69
|
-
"LT11": "layout.set_operators", "LT12": "layout.end-of-file",
|
|
70
|
-
"LT13": "layout.start_of_file", "LT14": "layout.one_line", "LT15": "layout.newlines",
|
|
71
|
-
# References
|
|
72
|
-
"RF01": "references.from", "RF02": "references.qualification",
|
|
73
|
-
"RF03": "references.consistent", "RF04": "references.keywords",
|
|
74
|
-
"RF05": "references.special_chars", "RF06": "references.quoting",
|
|
75
|
-
# Structure
|
|
76
|
-
"ST01": "structure.else_null", "ST02": "structure.simple_case",
|
|
77
|
-
"ST03": "structure.unused_cte", "ST04": "structure.nested_case",
|
|
78
|
-
"ST05": "structure.subquery", "ST06": "structure.column_order",
|
|
79
|
-
"ST07": "structure.using", "ST08": "structure.distinct",
|
|
80
|
-
"ST09": "structure.join_condition_order", "ST10": "structure.cte_definition_order",
|
|
81
|
-
"ST11": "structure.test_query",
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
# Core sub-keys that map to specific INI sections.
|
|
85
|
-
CORE_SECTION_MAP: dict[str, str] = {
|
|
86
|
-
"sqlfluff": "sqlfluff",
|
|
87
|
-
"dbt": "sqlfluff:templater:dbt",
|
|
88
|
-
"jinja": "sqlfluff:templater:jinja",
|
|
89
|
-
"templater": "sqlfluff:templater",
|
|
90
|
-
}
|
|
91
|
-
|
|
92
40
|
def __init__(self, snowflake_service: SnowflakeService, project_dir: Path):
|
|
93
41
|
self.sf = snowflake_service
|
|
94
42
|
self.project_dir = project_dir
|
|
95
|
-
|
|
96
|
-
# Options to strip from specific core sections (not valid sqlfluff config keys).
|
|
97
|
-
CORE_STRIP_OPTIONS: dict[str, set[str]] = {
|
|
98
|
-
"dbt": {"dbt_skip_compilation_error"},
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
@staticmethod
|
|
102
|
-
def _format_value(val) -> str:
|
|
103
|
-
"""Format a JSON value for sqlfluff INI output."""
|
|
104
|
-
if isinstance(val, bool):
|
|
105
|
-
return str(val)
|
|
106
|
-
if isinstance(val, list):
|
|
107
|
-
if not val:
|
|
108
|
-
return "None"
|
|
109
|
-
return ", ".join(str(v) for v in val)
|
|
110
|
-
if val is None or val == "":
|
|
111
|
-
return "None"
|
|
112
|
-
return str(val)
|
|
113
|
-
|
|
114
|
-
@staticmethod
|
|
115
|
-
def _emit_section(lines: list[str], header: str, options: dict) -> None:
|
|
116
|
-
"""Append an INI section with its options to lines."""
|
|
117
|
-
lines.append(f"[{header}]")
|
|
118
|
-
for key, val in options.items():
|
|
119
|
-
lines.append(f"{key} = {DownloadService._format_value(val)}")
|
|
120
|
-
lines.append("")
|
|
121
43
|
|
|
122
44
|
def download_sqlfluff_config(self, profiles_dir: str = None, dbt_project_dir: Path = None) -> bool:
|
|
123
45
|
info_line("Downloading SQLFluff configuration...")
|
|
124
|
-
|
|
125
|
-
if not
|
|
46
|
+
content = self.sf.get_sqlfluff_config_file()
|
|
47
|
+
if not content:
|
|
126
48
|
error_line("No SQLFluff configuration found in native app")
|
|
127
49
|
return False
|
|
128
|
-
|
|
129
|
-
lines: list[str] = []
|
|
130
50
|
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if opts:
|
|
149
|
-
self._emit_section(lines, section_header, opts)
|
|
150
|
-
|
|
151
|
-
# --- Indentation ---
|
|
152
|
-
indentation = raw.get("indentation", {})
|
|
153
|
-
for _code, entry in indentation.items():
|
|
154
|
-
if isinstance(entry, dict) and entry.get("enabled", True):
|
|
155
|
-
opts = entry.get("options", {})
|
|
156
|
-
if opts:
|
|
157
|
-
self._emit_section(lines, "sqlfluff:indentation", opts)
|
|
158
|
-
|
|
159
|
-
# --- Layout sections ([sqlfluff:layout:type:<code>]) ---
|
|
160
|
-
layout = raw.get("layout", {})
|
|
161
|
-
for code, entry in layout.items():
|
|
162
|
-
if not isinstance(entry, dict) or not entry.get("enabled", True):
|
|
163
|
-
continue
|
|
164
|
-
opts = entry.get("options", {})
|
|
165
|
-
if opts:
|
|
166
|
-
self._emit_section(lines, f"sqlfluff:layout:type:{code}", opts)
|
|
167
|
-
|
|
168
|
-
# --- Global rules ([sqlfluff:rules]) ---
|
|
169
|
-
rules = raw.get("rules", {})
|
|
170
|
-
for _code, entry in rules.items():
|
|
171
|
-
if isinstance(entry, dict) and entry.get("enabled", True):
|
|
172
|
-
opts = entry.get("options", {})
|
|
173
|
-
if opts:
|
|
174
|
-
self._emit_section(lines, "sqlfluff:rules", opts)
|
|
175
|
-
|
|
176
|
-
# --- Rule bundle sections (rules_aliasing, rules_capitalisation, etc.) ---
|
|
177
|
-
for section_key, entries in raw.items():
|
|
178
|
-
if not section_key.startswith("rules_") or not isinstance(entries, dict):
|
|
179
|
-
continue
|
|
180
|
-
for code, entry in entries.items():
|
|
181
|
-
if not isinstance(entry, dict) or not entry.get("enabled", True):
|
|
182
|
-
continue
|
|
183
|
-
section_name = self.RULE_SECTION_MAP.get(code)
|
|
184
|
-
if not section_name:
|
|
185
|
-
continue
|
|
186
|
-
opts = dict(entry.get("options", {}))
|
|
187
|
-
# For aliasing.length, 0 means "no limit" which sqlfluff expects as None
|
|
188
|
-
if section_name == "aliasing.length":
|
|
189
|
-
for len_key in ("min_alias_length", "max_alias_length"):
|
|
190
|
-
if len_key in opts and opts[len_key] == 0:
|
|
191
|
-
opts[len_key] = None
|
|
192
|
-
self._emit_section(lines, f"sqlfluff:rules:{section_name}", opts)
|
|
51
|
+
# If profiles_dir is specified, ensure it's set in the [sqlfluff:templater:dbt] section
|
|
52
|
+
if profiles_dir and "[sqlfluff:templater:dbt]" in content:
|
|
53
|
+
import re
|
|
54
|
+
# Replace existing profiles_dir line if present, otherwise insert after header
|
|
55
|
+
if re.search(r"^profiles_dir\s*=.*$", content, re.MULTILINE):
|
|
56
|
+
content = re.sub(
|
|
57
|
+
r"^profiles_dir\s*=.*$",
|
|
58
|
+
f"profiles_dir = {profiles_dir}",
|
|
59
|
+
content,
|
|
60
|
+
count=1,
|
|
61
|
+
flags=re.MULTILINE,
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
content = content.replace(
|
|
65
|
+
"[sqlfluff:templater:dbt]",
|
|
66
|
+
f"[sqlfluff:templater:dbt]\nprofiles_dir = {profiles_dir}",
|
|
67
|
+
)
|
|
193
68
|
|
|
194
|
-
content = "\n".join(lines)
|
|
195
69
|
dest = (dbt_project_dir or self.project_dir) / ".sqlfluff"
|
|
196
70
|
write_file(dest, content)
|
|
197
71
|
success_line(f"SQLFluff config written to {dest}")
|
{datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/snowflake_service.py
RENAMED
|
@@ -81,3 +81,8 @@ class SnowflakeService:
|
|
|
81
81
|
|
|
82
82
|
def get_support_contacts(self) -> list:
|
|
83
83
|
return self.call_procedure("get_support_contacts") or []
|
|
84
|
+
|
|
85
|
+
def get_sqlfluff_config_file(self) -> str:
|
|
86
|
+
"""Get the rendered .sqlfluff config INI text from the native app."""
|
|
87
|
+
result = self.call_procedure("get_sqlfluff_config_file")
|
|
88
|
+
return result if isinstance(result, str) else ""
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.5"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/bootstrap_service.py
RENAMED
|
File without changes
|
{datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/dbt_project_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/directory_scaffolder.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/linting_service.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/upstream_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
|
|
File without changes
|