datasecops-cli 0.3.0__tar.gz → 0.3.1__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 (48) hide show
  1. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/CHANGELOG.md +8 -1
  2. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/PKG-INFO +1 -1
  3. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/pyproject.toml +1 -1
  4. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/main.py +96 -23
  5. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/menus/downloads.py +1 -1
  6. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/models/project_config.py +1 -0
  7. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/services/download_service.py +6 -2
  8. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/services/snowflake_service.py +4 -1
  9. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/tests/test_main.py +2 -2
  10. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/.github/workflows/publish-cli.yml +0 -0
  11. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/.gitignore +0 -0
  12. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/DEVELOPMENT.md +0 -0
  13. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/LICENSE +0 -0
  14. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/README.md +0 -0
  15. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/docs/getting-started.md +0 -0
  16. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/docs/legacy.md +0 -0
  17. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/docs/legacy_plan_of_action.md +0 -0
  18. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/docs/mcp-server.md +0 -0
  19. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/mcp-servers.json +0 -0
  20. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/setup.ps1 +0 -0
  21. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/setup.sh +0 -0
  22. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/__init__.py +0 -0
  23. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/config.py +0 -0
  24. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/menus/__init__.py +0 -0
  25. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/menus/development.py +0 -0
  26. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/menus/git_operations.py +0 -0
  27. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/models/__init__.py +0 -0
  28. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/models/git_helpers.py +0 -0
  29. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/services/__init__.py +0 -0
  30. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/services/bootstrap_service.py +0 -0
  31. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/services/dbt_runner.py +0 -0
  32. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/services/git_service.py +0 -0
  33. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/services/linting_service.py +0 -0
  34. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/services/skill_service.py +0 -0
  35. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/utilities/__init__.py +0 -0
  36. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/utilities/display.py +0 -0
  37. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/utilities/file_utils.py +0 -0
  38. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
  39. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_mcp/__init__.py +0 -0
  40. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_mcp/__main__.py +0 -0
  41. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_mcp/connection.py +0 -0
  42. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/src/datasecops_mcp/server.py +0 -0
  43. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/tests/__init__.py +0 -0
  44. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/tests/test_config.py +0 -0
  45. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/tests/test_file_utils.py +0 -0
  46. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/tests/test_models.py +0 -0
  47. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/tests/test_version.py +0 -0
  48. {datasecops_cli-0.3.0 → datasecops_cli-0.3.1}/tests/test_yaml_utils.py +0 -0
@@ -7,10 +7,17 @@ All notable changes to the DataSecOps CLI are documented in this file.
7
7
  ### Added
8
8
 
9
9
  - **`datasecops setup` subcommand** — pure-Python replacement for `setup.ps1` / `setup.sh`. Discovers Snowflake connections from `~/.snowflake/connections.toml`, prompts for connection and app database, and writes `.datasecops.yml`. No platform-specific scripts needed.
10
- - **Interactive profile selection** — when `profile_name` is not set (no `dbt_project.yml` and no profile in `.datasecops.yml`), the CLI prompts the user to choose from available profiles in the native app instead of silently picking the first one
10
+ - **Prerequisite checks during setup** — checks for `dbtf`, `gh`, and `az` on PATH and verifies authentication status (`gh auth status`, `az account show`)
11
+ - **Interactive profile selection** — when `profile_name` is not set (no `dbt_project.yml` and no profile in `.datasecops.yml`), the CLI prompts the user to choose from available profiles in the native app instead of silently picking the first one. Selected profile is saved back to `.datasecops.yml`.
11
12
  - **Auto-setup on first run** — running `datasecops` without a `.datasecops.yml` now offers to run setup interactively instead of just exiting with an error
13
+ - **`--connection`, `--app-database`, `--profile` flags on `download`** — allows CI/CD pipelines to run without a committed `.datasecops.yml` by passing connection details as CLI flags (e.g. `datasecops download sqlfluff -c ci -d MY_APP_DB -p my_project`)
14
+ - **Default app database value** — setup now defaults to `DATA_ENGINEERS_DATASECOPS_FRAMEWORK` (press Enter to accept)
12
15
  - **dbt project directory resolution from framework** — `dbt_project_dir` is now re-resolved using the framework's `project_dir` setting after native app config is loaded
13
16
 
17
+ ### Changed
18
+
19
+ - **Non-interactive mode safety** — `_connect_and_load` accepts an `interactive` flag; the `download` subcommand passes `interactive=False` so it never prompts (missing config exits 1, multiple profiles use the first one silently)
20
+
14
21
  ## [0.2.9] - 2026-05-14
15
22
 
16
23
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datasecops-cli
3
- Version: 0.3.0
3
+ Version: 0.3.1
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.3.0"
7
+ version = "0.3.1"
8
8
  description = "DataSecOps Framework CLI for Snowflake Native App"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -46,6 +46,18 @@ def _build_parser() -> argparse.ArgumentParser:
46
46
  choices=DOWNLOAD_ITEMS,
47
47
  help="Item(s) to download/install: sqlfluff, pipelines, packages, macros, install-sqlfluff, install-dbt, or all",
48
48
  )
49
+ dl.add_argument(
50
+ "--connection", "-c",
51
+ help="Snowflake connection name (overrides .datasecops.yml)",
52
+ )
53
+ dl.add_argument(
54
+ "--app-database", "-d",
55
+ help="Native app database name (overrides .datasecops.yml)",
56
+ )
57
+ dl.add_argument(
58
+ "--profile", "-p",
59
+ help="Project profile name (overrides .datasecops.yml)",
60
+ )
49
61
 
50
62
  return parser
51
63
 
@@ -62,7 +74,9 @@ def main():
62
74
  elif args.command == "bootstrap":
63
75
  _run_bootstrap(config)
64
76
  elif args.command == "download":
65
- _run_download(config, args.items)
77
+ _run_download(config, args.items,
78
+ connection=args.connection, app_database=args.app_database,
79
+ profile=args.profile)
66
80
  else:
67
81
  _run_interactive(config)
68
82
 
@@ -70,11 +84,40 @@ def main():
70
84
  def _run_setup(project_dir: Path):
71
85
  """Interactive setup: create .datasecops.yml by prompting for connection and app database."""
72
86
  from datasecops_cli.utilities.display import (
73
- get_input_string, select_from_list
87
+ get_input_string, get_input_string_or_default, select_from_list, warning_line
74
88
  )
75
89
 
76
90
  section_header("DataSecOps Setup")
77
91
 
92
+ # --- Check prerequisites ---
93
+ info_line("Checking prerequisites...")
94
+
95
+ if shutil.which("dbtf"):
96
+ success_line("dbt Fusion (dbtf) found")
97
+ else:
98
+ warning_line("dbt Fusion (dbtf) not found — dbt commands will not work")
99
+ warning_line(" Install: https://docs.getdbt.com/docs/core/installation")
100
+
101
+ if shutil.which("gh"):
102
+ result = subprocess.run(["gh", "auth", "status"], capture_output=True, text=True)
103
+ if result.returncode == 0:
104
+ success_line("GitHub CLI (gh) found and authenticated")
105
+ else:
106
+ warning_line("GitHub CLI (gh) found but not logged in — run: gh auth login")
107
+ else:
108
+ warning_line("GitHub CLI (gh) not found — install from https://cli.github.com if using GitHub")
109
+
110
+ if shutil.which("az"):
111
+ result = subprocess.run(["az", "account", "show"], capture_output=True, text=True)
112
+ if result.returncode == 0:
113
+ success_line("Azure CLI (az) found and authenticated")
114
+ else:
115
+ warning_line("Azure CLI (az) found but not logged in — run: az login")
116
+ else:
117
+ warning_line("Azure CLI (az) not found — install from https://aka.ms/installazurecli if using Azure DevOps")
118
+
119
+ info_line("")
120
+
78
121
  # --- Discover Snowflake connections ---
79
122
  connections_file = Path.home() / ".snowflake" / "connections.toml"
80
123
  connection_names = []
@@ -96,12 +139,10 @@ def _run_setup(project_dir: Path):
96
139
  sys.exit(1)
97
140
 
98
141
  # --- App database ---
99
- app_database = get_input_string(
100
- "Enter native app database name (e.g. DATA_ENGINEERS_DATASECOPS_FRAMEWORK): "
142
+ app_database = get_input_string_or_default(
143
+ "Enter native app database name",
144
+ "DATA_ENGINEERS_DATASECOPS_FRAMEWORK"
101
145
  )
102
- if not app_database:
103
- error_line("App database name is required")
104
- sys.exit(1)
105
146
 
106
147
  # --- Write config ---
107
148
  config_data = {
@@ -124,15 +165,15 @@ def _offer_setup(project_dir: Path):
124
165
  _run_setup(project_dir)
125
166
 
126
167
 
127
- def _connect_and_load(config: Config) -> SnowflakeService:
168
+ def _connect_and_load(config: Config, interactive: bool = True) -> SnowflakeService:
128
169
  """Load config, connect to Snowflake, and load native app settings.
129
170
 
130
171
  Returns the connected SnowflakeService, or calls sys.exit on failure.
131
- If .datasecops.yml is missing and a setup script exists, offers to run it.
172
+ If .datasecops.yml is missing and interactive is True, offers to run setup.
132
173
  """
133
174
  if not config.load():
134
175
  # Check if the failure is due to missing .datasecops.yml
135
- if not (config.project_dir / ".datasecops.yml").exists():
176
+ if not (config.project_dir / ".datasecops.yml").exists() and interactive:
136
177
  _offer_setup(config.project_dir)
137
178
  # Retry after setup
138
179
  if not config.load():
@@ -157,16 +198,29 @@ def _connect_and_load(config: Config) -> SnowflakeService:
157
198
 
158
199
  # If profile_name was not explicitly set and there are multiple profiles, ask the user
159
200
  if not profile_was_set and len(config.all_profiles) > 1:
160
- from datasecops_cli.utilities.display import select_from_list
161
- names = [p.profile_name for p in config.all_profiles]
162
- info_line(f"Found {len(names)} project profiles:")
163
- selected = select_from_list(names, "profile", add_back=False)
164
- config.profile = None
165
- for p in config.all_profiles:
166
- if p.profile_name == selected:
167
- config.profile = p
168
- config.profile_name = selected
169
- break
201
+ if interactive:
202
+ from datasecops_cli.utilities.display import select_from_list
203
+ names = [p.profile_name for p in config.all_profiles]
204
+ info_line(f"Found {len(names)} project profiles:")
205
+ selected = select_from_list(names, "profile", add_back=False)
206
+ config.profile = None
207
+ for p in config.all_profiles:
208
+ if p.profile_name == selected:
209
+ config.profile = p
210
+ config.profile_name = selected
211
+ break
212
+ # Save the selected profile back to .datasecops.yml
213
+ if config.profile:
214
+ config_data = {
215
+ "connection_name": config.datasecops.connection_name,
216
+ "app_database": config.datasecops.app_database,
217
+ "profile_name": config.profile_name,
218
+ }
219
+ write_datasecops_config(config.project_dir, config_data)
220
+ success_line(f"Saved profile_name '{config.profile_name}' to .datasecops.yml")
221
+ else:
222
+ # Non-interactive: use the auto-selected first profile
223
+ info_line(f"Using default profile: {config.profile_name}")
170
224
 
171
225
  # Re-resolve dbt_project_dir using framework project_dir setting
172
226
  framework_dbt_dir = config.project_dir / config.project_settings.project_dir
@@ -216,9 +270,28 @@ def _run_interactive(config: Config):
216
270
  sf_service.close()
217
271
 
218
272
 
219
- def _run_download(config: Config, items: list[str]):
273
+ def _run_download(config: Config, items: list[str],
274
+ connection: str = None, app_database: str = None,
275
+ profile: str = None):
220
276
  """Run non-interactive downloads for CI/CD pipelines."""
221
- sf_service = _connect_and_load(config)
277
+ # If CLI flags provided, write/override .datasecops.yml on the fly
278
+ if connection or app_database:
279
+ from datasecops_cli.utilities.yaml_utils import read_datasecops_config
280
+ existing = read_datasecops_config(config.project_dir) or {}
281
+ config_data = {
282
+ "connection_name": connection or existing.get("connection_name", ""),
283
+ "app_database": app_database or existing.get("app_database", ""),
284
+ "profile_name": profile or existing.get("profile_name", ""),
285
+ }
286
+ write_datasecops_config(config.project_dir, config_data)
287
+ info_line(f"Wrote .datasecops.yml (connection={config_data['connection_name']}, app_database={config_data['app_database']})")
288
+ elif profile:
289
+ from datasecops_cli.utilities.yaml_utils import read_datasecops_config
290
+ existing = read_datasecops_config(config.project_dir) or {}
291
+ existing["profile_name"] = profile
292
+ write_datasecops_config(config.project_dir, existing)
293
+
294
+ sf_service = _connect_and_load(config, interactive=False)
222
295
 
223
296
  try:
224
297
  download_service = DownloadService(sf_service, config.project_dir)
@@ -240,7 +313,7 @@ def _run_download(config: Config, items: list[str]):
240
313
  elif item == "pipelines":
241
314
  platform = config.source_control.source_control_platform.lower()
242
315
  info_line(f"Platform: {platform}")
243
- if not download_service.download_pipelines(platform=platform):
316
+ if not download_service.download_pipelines(platform=platform, profile_name=config.profile_name):
244
317
  failed = True
245
318
  elif item == "packages":
246
319
  if not download_service.download_dbt_packages(config.dbt_project_dir):
@@ -38,7 +38,7 @@ class DownloadsMenu:
38
38
  display_action_header("Download Pipeline Files")
39
39
  platform = self.source_control.source_control_platform.lower() if self.source_control else "github"
40
40
  info_line(f"Platform: {platform}")
41
- self.downloads.download_pipelines(platform=platform)
41
+ self.downloads.download_pipelines(platform=platform, profile_name=self.profile_name)
42
42
  complete_action()
43
43
  elif option == 3:
44
44
  display_action_header("Download dbt Packages")
@@ -65,6 +65,7 @@ class ProjectProfile(BaseModel):
65
65
  target_database: str = ""
66
66
  dbt_version: str = "1.9"
67
67
  dbt_packages: list[str] = Field(default_factory=list)
68
+ pipeline_ids: list[str] = Field(default_factory=list)
68
69
 
69
70
  class DbtPackage(BaseModel):
70
71
  name: str = ""
@@ -220,9 +220,13 @@ class DownloadService:
220
220
  error_line("No active dbt versions found in framework configuration")
221
221
  return dbt_packages
222
222
 
223
- def download_pipelines(self, platform: str = "github") -> bool:
223
+ def download_pipelines(self, platform: str = "github", profile_name: str = "") -> bool:
224
224
  info_line(f"Downloading {platform} pipeline configurations...")
225
- raw = self.sf.get_framework_config("PIPELINES")
225
+ if profile_name:
226
+ info_line(f"Filtering pipelines for profile: {profile_name}")
227
+ raw = self.sf.get_pipelines_for_profile(profile_name)
228
+ else:
229
+ raw = self.sf.get_framework_config("PIPELINES")
226
230
  if not raw:
227
231
  error_line("No pipeline configuration found in native app")
228
232
  return False
@@ -59,7 +59,10 @@ class SnowflakeService:
59
59
 
60
60
  def get_project_profiles(self) -> Optional[list[dict]]:
61
61
  return self.call_procedure("get_project_profiles")
62
-
62
+
63
+ def get_pipelines_for_profile(self, profile_name: str) -> Optional[dict]:
64
+ return self.call_procedure("get_pipelines_for_profile", profile_name)
65
+
63
66
  def get_account_info(self) -> dict:
64
67
  rows = self.execute_query("SELECT CURRENT_ACCOUNT_NAME() as account, CURRENT_USER() as user_name")
65
68
  return rows[0] if rows else {}
@@ -102,7 +102,7 @@ class TestRunDownload:
102
102
  _run_download(config, ["pipelines"])
103
103
 
104
104
  assert exc.value.code == 0
105
- mock_ds.download_pipelines.assert_called_once_with(platform="github")
105
+ mock_ds.download_pipelines.assert_called_once_with(platform="github", profile_name="test_profile")
106
106
 
107
107
  @patch("datasecops_cli.main._connect_and_load")
108
108
  def test_download_packages(self, mock_connect, tmp_path):
@@ -219,7 +219,7 @@ class TestRunDownload:
219
219
  _run_download(config, ["pipelines"])
220
220
 
221
221
  assert exc.value.code == 0
222
- mock_ds.download_pipelines.assert_called_once_with(platform="azuredevops")
222
+ mock_ds.download_pipelines.assert_called_once_with(platform="azuredevops", profile_name="test_profile")
223
223
 
224
224
  @patch("datasecops_cli.main._connect_and_load")
225
225
  def test_install_sqlfluff(self, mock_connect, tmp_path):
File without changes
File without changes
File without changes
File without changes