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.
Files changed (54) hide show
  1. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/CHANGELOG.md +11 -0
  2. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/PKG-INFO +1 -1
  3. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/pyproject.toml +1 -1
  4. datasecops_cli-0.4.6/src/datasecops_cli/__init__.py +1 -0
  5. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/menus/development.py +6 -0
  6. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/download_service.py +19 -145
  7. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/snowflake_service.py +5 -0
  8. datasecops_cli-0.4.5/src/datasecops_cli/__init__.py +0 -1
  9. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/.github/workflows/auto-tag.yml +0 -0
  10. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/.github/workflows/publish-cli.yml +0 -0
  11. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/.gitignore +0 -0
  12. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/DEVELOPMENT.md +0 -0
  13. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/LICENSE +0 -0
  14. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/README.md +0 -0
  15. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/docs/getting-started.md +0 -0
  16. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/docs/legacy.md +0 -0
  17. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/docs/legacy_plan_of_action.md +0 -0
  18. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/docs/mcp-server.md +0 -0
  19. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/mcp-servers.json +0 -0
  20. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/setup.ps1 +0 -0
  21. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/setup.sh +0 -0
  22. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/config.py +0 -0
  23. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/main.py +0 -0
  24. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/menus/__init__.py +0 -0
  25. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/menus/configuration.py +0 -0
  26. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/menus/downloads.py +0 -0
  27. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/menus/git_operations.py +0 -0
  28. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/models/__init__.py +0 -0
  29. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/models/git_helpers.py +0 -0
  30. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/models/project_config.py +0 -0
  31. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/__init__.py +0 -0
  32. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/bootstrap_service.py +0 -0
  33. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/dbt_project_generator.py +0 -0
  34. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/dbt_runner.py +0 -0
  35. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/directory_scaffolder.py +0 -0
  36. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/git_service.py +0 -0
  37. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/linting_service.py +0 -0
  38. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/skill_service.py +0 -0
  39. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/services/upstream_service.py +0 -0
  40. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/utilities/__init__.py +0 -0
  41. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/utilities/display.py +0 -0
  42. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/utilities/file_utils.py +0 -0
  43. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
  44. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_mcp/__init__.py +0 -0
  45. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_mcp/__main__.py +0 -0
  46. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_mcp/connection.py +0 -0
  47. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/src/datasecops_mcp/server.py +0 -0
  48. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/tests/__init__.py +0 -0
  49. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/tests/test_config.py +0 -0
  50. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/tests/test_file_utils.py +0 -0
  51. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/tests/test_main.py +0 -0
  52. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/tests/test_models.py +0 -0
  53. {datasecops_cli-0.4.5 → datasecops_cli-0.4.6}/tests/test_version.py +0 -0
  54. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datasecops-cli
3
- Version: 0.4.5
3
+ Version: 0.4.6
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.4.5"
7
+ version = "0.4.6"
8
8
  description = "DataSecOps Framework CLI for Snowflake Native App"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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:
@@ -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
- raw = self.sf.get_framework_config("SQLFLUFF_RULES")
125
- if not raw:
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
- # --- Core sections ([sqlfluff], [sqlfluff:templater:dbt], etc.) ---
132
- core = raw.get("core", {})
133
- for key, section_header in self.CORE_SECTION_MAP.items():
134
- entry = core.get(key)
135
- if not isinstance(entry, dict) or not entry.get("enabled", True):
136
- continue
137
- opts = dict(entry.get("options", {}))
138
- # Strip options that are not valid sqlfluff config keys
139
- for strip_key in self.CORE_STRIP_OPTIONS.get(key, set()):
140
- opts.pop(strip_key, None)
141
- if key == "dbt":
142
- # Override profiles_dir / project_dir with local values
143
- opts["project_dir"] = "."
144
- if profiles_dir:
145
- opts["profiles_dir"] = profiles_dir
146
- else:
147
- opts.pop("profiles_dir", None)
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}")
@@ -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