datasecops-cli 0.4.7__tar.gz → 0.4.8__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.7 → datasecops_cli-0.4.8}/CHANGELOG.md +10 -0
  2. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/PKG-INFO +1 -1
  3. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/pyproject.toml +1 -1
  4. datasecops_cli-0.4.8/src/datasecops_cli/__init__.py +1 -0
  5. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/main.py +33 -0
  6. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/menus/configuration.py +17 -0
  7. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/models/project_config.py +1 -0
  8. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/services/dbt_runner.py +63 -5
  9. datasecops_cli-0.4.7/src/datasecops_cli/__init__.py +0 -1
  10. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/.github/workflows/auto-tag.yml +0 -0
  11. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/.github/workflows/publish-cli.yml +0 -0
  12. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/.gitignore +0 -0
  13. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/DEVELOPMENT.md +0 -0
  14. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/LICENSE +0 -0
  15. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/README.md +0 -0
  16. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/docs/getting-started.md +0 -0
  17. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/docs/legacy.md +0 -0
  18. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/docs/legacy_plan_of_action.md +0 -0
  19. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/docs/mcp-server.md +0 -0
  20. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/mcp-servers.json +0 -0
  21. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/setup.ps1 +0 -0
  22. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/setup.sh +0 -0
  23. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/config.py +0 -0
  24. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/menus/__init__.py +0 -0
  25. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/menus/development.py +0 -0
  26. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/menus/downloads.py +0 -0
  27. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/menus/git_operations.py +0 -0
  28. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/models/__init__.py +0 -0
  29. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/models/git_helpers.py +0 -0
  30. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/services/__init__.py +0 -0
  31. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/services/bootstrap_service.py +0 -0
  32. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/services/dbt_project_generator.py +0 -0
  33. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/services/directory_scaffolder.py +0 -0
  34. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/services/download_service.py +0 -0
  35. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/services/git_service.py +0 -0
  36. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/services/linting_service.py +0 -0
  37. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/services/skill_service.py +0 -0
  38. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/services/snowflake_service.py +0 -0
  39. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/services/upstream_service.py +0 -0
  40. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/utilities/__init__.py +0 -0
  41. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/utilities/display.py +0 -0
  42. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/utilities/file_utils.py +0 -0
  43. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
  44. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_mcp/__init__.py +0 -0
  45. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_mcp/__main__.py +0 -0
  46. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_mcp/connection.py +0 -0
  47. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/src/datasecops_mcp/server.py +0 -0
  48. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/tests/__init__.py +0 -0
  49. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/tests/test_config.py +0 -0
  50. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/tests/test_file_utils.py +0 -0
  51. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/tests/test_main.py +0 -0
  52. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/tests/test_models.py +0 -0
  53. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/tests/test_version.py +0 -0
  54. {datasecops_cli-0.4.7 → datasecops_cli-0.4.8}/tests/test_yaml_utils.py +0 -0
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to the DataSecOps CLI are documented in this file.
4
4
 
5
+ ## [0.4.8] - 2026-05-20
6
+
7
+ ### Added
8
+
9
+ - **dbt Fusion update menu option** — new `[7] dbtf update` in the Configuration menu runs `dbtf system update` to upgrade dbt Fusion to the latest version. Checks that `dbtf` is on PATH before attempting the update.
10
+
11
+ ### Fixed
12
+
13
+ - **`dbt docs generate` replaced with `dbt compile --write-catalog` for dbt Fusion** — dbt Fusion does not support `dbt docs generate`. The docs menu option now runs `dbtf compile --write-catalog` when using dbt Fusion, writing `catalog.json` to the target directory. Serving docs automatically downloads `index.html` from dbt-core if missing, launches `python -m http.server 8080` in a new terminal window, and opens the browser at `http://localhost:8080`. The CLI is not blocked — close the server terminal when done.
14
+
5
15
  ## [0.4.7] - 2026-05-19
6
16
 
7
17
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datasecops-cli
3
- Version: 0.4.7
3
+ Version: 0.4.8
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.7"
7
+ version = "0.4.8"
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.8"
@@ -196,6 +196,14 @@ def _run_setup(project_dir: Path):
196
196
  if raw_sc:
197
197
  source_control = SourceControl(**{k: v for k, v in raw_sc.items() if k in SourceControl.model_fields})
198
198
 
199
+ # Get account identifier for MCP URL construction
200
+ account_identifier = ""
201
+ try:
202
+ org_rows = temp_sf.execute_query("SELECT CURRENT_ORGANIZATION_NAME() || '-' || CURRENT_ACCOUNT_NAME() AS acct")
203
+ account_identifier = org_rows[0]["ACCT"].lower() if org_rows else ""
204
+ except Exception as e:
205
+ warning_line(f"Could not determine account identifier (AI Agent MCP will be skipped): {e}")
206
+
199
207
  # --- Resolve profile name ---
200
208
  from datasecops_cli.utilities.yaml_utils import get_profile_name
201
209
  profile_name = get_profile_name(project_dir)
@@ -286,6 +294,11 @@ def _run_setup(project_dir: Path):
286
294
  platform_choice = source_control.source_control_platform
287
295
  info_line(f"Platform: {platform_choice}")
288
296
 
297
+ # Build AI Agent MCP URL once (used in both Cortex Code and mcp.json paths)
298
+ ai_agent_mcp_url = ""
299
+ if project_settings.ai_agents_enabled and account_identifier:
300
+ ai_agent_mcp_url = f"https://{account_identifier}.snowflakecomputing.com/api/v2/databases/{app_database}/schemas/AI/mcp-servers/DATASECOPS_MCP"
301
+
289
302
  info_line("")
290
303
  info_line("Which AI tool are you configuring?")
291
304
  tool_options = ["VS Code (Copilot)", "Cursor", "Claude Code"]
@@ -304,6 +317,18 @@ def _run_setup(project_dir: Path):
304
317
  except FileNotFoundError:
305
318
  warning_line("cortex not found — run manually: cortex mcp add datasecops-framework -- datasecops-mcp")
306
319
 
320
+ # Add AI Agent MCP server if enabled
321
+ if ai_agent_mcp_url:
322
+ try:
323
+ result = subprocess.run(["cortex", "mcp", "add", "datasecops-docs", ai_agent_mcp_url, "--transport", "http"],
324
+ capture_output=True, text=True)
325
+ if result.returncode == 0:
326
+ success_line("Added datasecops-docs MCP server (AI Agent documentation search)")
327
+ else:
328
+ warning_line(f"Failed to add AI Agent MCP server: {result.stderr.strip() or 'unknown error'}")
329
+ except Exception as agent_err:
330
+ warning_line(f"Could not add AI Agent MCP server: {agent_err}")
331
+
307
332
  if platform_choice == "GitHub":
308
333
  github_token = get_input_string("Enter your GitHub PAT (or press Enter to skip): ", allow_empty=True)
309
334
  if github_token:
@@ -343,6 +368,14 @@ def _run_setup(project_dir: Path):
343
368
 
344
369
  servers = {"datasecops-framework": {"command": "datasecops-mcp", "args": []}}
345
370
 
371
+ # Add AI Agent MCP server if enabled
372
+ if ai_agent_mcp_url:
373
+ servers["datasecops-docs"] = {
374
+ "url": ai_agent_mcp_url,
375
+ "headers": {"Authorization": "Bearer <YOUR_PAT>"},
376
+ }
377
+ info_line("Note: Replace <YOUR_PAT> in datasecops-docs headers with your Snowflake PAT")
378
+
346
379
  if platform_choice == "GitHub":
347
380
  servers["github"] = {
348
381
  "command": "npx",
@@ -51,6 +51,8 @@ class ConfigurationMenu:
51
51
  self._cortex_upgrade()
52
52
  elif option == 6:
53
53
  self._toggle_dbt_engine()
54
+ elif option == 7:
55
+ self._dbtf_update()
54
56
  self._menu()
55
57
  option = get_input_number("Choose an option: ")
56
58
 
@@ -64,6 +66,7 @@ class ConfigurationMenu:
64
66
  menu_option(4, "new project - Initialize a new dbt project with framework profiles")
65
67
  menu_option(5, "cortex update - Update Cortex Code to the latest version")
66
68
  menu_option(6, f"dbt engine - Switch dbt engine (current: {engine_label})")
69
+ menu_option(7, "dbtf update - Update dbt Fusion to the latest version")
67
70
  menu_option(0, "back - Return to main menu")
68
71
 
69
72
  def _install_dbt_requirements(self) -> None:
@@ -288,3 +291,17 @@ class ConfigurationMenu:
288
291
  success_line(f"dbt engine switched to {self.datasecops_config.get_engine_label()} ({new_engine})")
289
292
  warning_line("Restart the CLI for the change to take effect.")
290
293
  complete_action()
294
+
295
+ def _dbtf_update(self) -> None:
296
+ """Run dbtf system update to upgrade to the latest version."""
297
+ display_action_header("Update dbt Fusion")
298
+ if not shutil.which("dbtf"):
299
+ error_line("dbt Fusion (dbtf) not found. Install it first.")
300
+ complete_action()
301
+ return
302
+ try:
303
+ info_line("Checking for updates...")
304
+ subprocess.run(["dbtf", "system", "update"])
305
+ except FileNotFoundError:
306
+ error_line("dbt Fusion (dbtf) not found.")
307
+ complete_action()
@@ -29,6 +29,7 @@ class ProjectSettings(BaseModel):
29
29
  project_dir: str = "./dbt"
30
30
  profile_dir: str = "~/.dbt"
31
31
  execution_mode: str = "dbt_cli"
32
+ ai_agents_enabled: bool = False
32
33
  targets: list[DbtTarget] = Field(default_factory=lambda: [
33
34
  DbtTarget(target_name="dev", branch_name="dev", target_role="DEVELOPERS", target_warehouse="DEV_WH", is_default=True),
34
35
  DbtTarget(target_name="test", branch_name="test", target_role="DATAOPS_ADMIN", target_warehouse="DATAOPS_WH"),
@@ -1,9 +1,13 @@
1
1
  import subprocess
2
+ import shlex
2
3
  import shutil
4
+ import sys
5
+ import urllib.request
6
+ import webbrowser
3
7
  from pathlib import Path
4
8
  from typing import Optional
5
9
 
6
- from datasecops_cli.utilities.display import info_line, error_line, success_line
10
+ from datasecops_cli.utilities.display import info_line, error_line, success_line, warning_line
7
11
 
8
12
 
9
13
  class DbtRunner:
@@ -93,12 +97,66 @@ class DbtRunner:
93
97
  return self._run_command("source", ["freshness"] + args)
94
98
 
95
99
  def docs_generate(self) -> subprocess.CompletedProcess:
100
+ if self.engine == "dbtf":
101
+ return self._run_command("compile", ["--write-catalog", f"--target={self.target}"])
96
102
  return self._run_command("docs", ["generate", f"--target={self.target}"])
97
103
 
98
- def docs_serve(self) -> subprocess.Popen:
99
- cmd = [self.engine, "docs", "serve"] + self._default_args()
100
- info_line(f"Running: {' '.join(cmd)}")
101
- return subprocess.Popen(cmd)
104
+ def docs_serve(self, port: int = 8080) -> None:
105
+ if self.engine == "dbtf":
106
+ target_dir = self.project_dir / "target"
107
+ index_file = target_dir / "index.html"
108
+ if not index_file.exists():
109
+ info_line("Downloading dbt docs index.html...")
110
+ url = "https://raw.githubusercontent.com/dbt-labs/dbt-core/main/core/dbt/task/docs/index.html"
111
+ try:
112
+ urllib.request.urlretrieve(url, str(index_file))
113
+ success_line("Downloaded index.html to target/")
114
+ except Exception as e:
115
+ error_line(f"Failed to download index.html: {e}")
116
+ warning_line("Download manually from: https://github.com/dbt-labs/dbt-core/blob/main/core/dbt/task/docs/index.html")
117
+ return
118
+ # Launch HTTP server in a new terminal window
119
+ cmd = [sys.executable, "-m", "http.server", str(port)]
120
+ try:
121
+ if sys.platform == "win32":
122
+ subprocess.Popen( # noqa: S603 - all args are internal/hardcoded
123
+ ["cmd", "/c", "start", f"dbt docs - localhost:{port}"] + cmd,
124
+ cwd=str(target_dir),
125
+ )
126
+ elif sys.platform == "darwin":
127
+ subprocess.Popen( # noqa: S603 - all args are internal/hardcoded
128
+ ["open", "-a", "Terminal", "--args"] + cmd,
129
+ cwd=str(target_dir),
130
+ )
131
+ else:
132
+ # Linux — use shlex.quote and bash -lc for safe argument passing
133
+ cmd_str = " ".join(shlex.quote(arg) for arg in cmd)
134
+ for term in ("x-terminal-emulator", "gnome-terminal", "xterm"):
135
+ if not shutil.which(term):
136
+ continue
137
+ if term == "gnome-terminal":
138
+ term_cmd = [term, "--", "bash", "-lc", cmd_str]
139
+ else:
140
+ term_cmd = [term, "-e", "bash", "-lc", cmd_str]
141
+ subprocess.Popen(term_cmd, cwd=str(target_dir)) # noqa: S603
142
+ break
143
+ else:
144
+ # Fallback: run in background
145
+ subprocess.Popen(cmd, cwd=str(target_dir)) # noqa: S603
146
+ except (OSError, subprocess.SubprocessError) as e:
147
+ error_line(f"Failed to start docs server: {e}")
148
+ warning_line("Try manually: python -m http.server 8080 (from the target/ directory)")
149
+ return
150
+ info_line(f"Docs server started at http://localhost:{port}")
151
+ webbrowser.open(f"http://localhost:{port}")
152
+ success_line("Opened docs in browser")
153
+ else:
154
+ cmd = [self.engine, "docs", "serve"] + self._default_args()
155
+ info_line(f"Running: {' '.join(cmd)}")
156
+ try:
157
+ subprocess.Popen(cmd) # noqa: S603 - cmd is internal (engine + hardcoded args)
158
+ except (OSError, subprocess.SubprocessError) as e:
159
+ error_line(f"Failed to start docs server: {e}")
102
160
 
103
161
  def clean(self) -> subprocess.CompletedProcess:
104
162
  return self._run_command("clean")
@@ -1 +0,0 @@
1
- __version__ = "0.4.7"
File without changes
File without changes
File without changes
File without changes