datasecops-cli 0.5.8__tar.gz → 0.5.10__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.5.8 → datasecops_cli-0.5.10}/CHANGELOG.md +15 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/PKG-INFO +2 -2
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/README.md +1 -1
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/pyproject.toml +1 -1
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/menus/development.py +9 -5
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/menus/git_operations.py +40 -1
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/dbt_runner.py +2 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/git_service.py +26 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/linting_service.py +15 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/.github/workflows/auto-tag.yml +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/.github/workflows/publish-cli.yml +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/.github/workflows/test.yml +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/.gitignore +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/DEVELOPMENT.md +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/LICENSE +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/docs/getting-started.md +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/docs/git-operations.md +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/docs/legacy.md +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/docs/legacy_plan_of_action.md +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/docs/mcp-server.md +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/mcp-servers.json +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/setup.ps1 +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/setup.sh +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/__init__.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/config.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/main.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/menus/__init__.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/menus/configuration.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/menus/downloads.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/models/__init__.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/models/git_helpers.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/models/project_config.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/__init__.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/bootstrap_service.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/dbt_project_generator.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/directory_scaffolder.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/download_service.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/skill_service.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/snowflake_service.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/upstream_service.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/utilities/__init__.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/utilities/display.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/utilities/file_utils.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/utilities/mcp.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_mcp/__init__.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_mcp/__main__.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_mcp/connection.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_mcp/server.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/tests/__init__.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/tests/test_config.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/tests/test_file_utils.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/tests/test_main.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/tests/test_models.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/tests/test_version.py +0 -0
- {datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/tests/test_yaml_utils.py +0 -0
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the DataSecOps CLI are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.10] - 2026-06-26
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **Lint modified includes all uncommitted files** — "lint modified" and "fix modified" now detect staged changes, unstaged changes, and untracked files (previously only caught unstaged modifications).
|
|
10
|
+
- **Lint specific file accepts model names** — the "specific" lint option now accepts a model name (e.g. `stg_orders`) and resolves it to the SQL file path by searching the models directory recursively.
|
|
11
|
+
- **Reset branch from main** — new git menu option (10) to force-reset an environment branch (e.g. `test`) to match `origin/main`, removing all existing commits on that branch.
|
|
12
|
+
- **Deploy excludes main branch** — the deploy-to-environment menu no longer lists `main` as a target, preventing accidental force-pushes to the primary branch.
|
|
13
|
+
|
|
14
|
+
## [0.5.9] - 2026-06-22
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **`dbt seed` scoped to current project** — `seed` now passes `--select <project_name>` by default (matching `run`, `compile`, `build`, and `test`), preventing package seeds from being loaded.
|
|
19
|
+
|
|
5
20
|
## [0.5.8] - 2026-06-22
|
|
6
21
|
|
|
7
22
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datasecops-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.10
|
|
4
4
|
Summary: DataSecOps Framework CLI for Snowflake Native App
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -81,7 +81,7 @@ datasecops
|
|
|
81
81
|
| Menu | Capabilities |
|
|
82
82
|
|------|-------------|
|
|
83
83
|
| **Development** | dbt run, build, test, lint (SQLFluff), deps, seed, compile, snapshot, freshness, docs |
|
|
84
|
-
| **Git** | Branch create/checkout/delete, commit & push, rebase, squash, deploy to environment branches, cherry-pick |
|
|
84
|
+
| **Git** | Branch create/checkout/delete, commit & push, rebase, squash, deploy to environment branches, cherry-pick, reset branch from main |
|
|
85
85
|
| **Downloads** | SQLFluff config, CI/CD pipelines (GitHub Actions / Azure DevOps), dbt packages, Cortex Code skills |
|
|
86
86
|
|
|
87
87
|
## Non-Interactive Mode (CI/CD)
|
|
@@ -62,7 +62,7 @@ datasecops
|
|
|
62
62
|
| Menu | Capabilities |
|
|
63
63
|
|------|-------------|
|
|
64
64
|
| **Development** | dbt run, build, test, lint (SQLFluff), deps, seed, compile, snapshot, freshness, docs |
|
|
65
|
-
| **Git** | Branch create/checkout/delete, commit & push, rebase, squash, deploy to environment branches, cherry-pick |
|
|
65
|
+
| **Git** | Branch create/checkout/delete, commit & push, rebase, squash, deploy to environment branches, cherry-pick, reset branch from main |
|
|
66
66
|
| **Downloads** | SQLFluff config, CI/CD pipelines (GitHub Actions / Azure DevOps), dbt packages, Cortex Code skills |
|
|
67
67
|
|
|
68
68
|
## Non-Interactive Mode (CI/CD)
|
|
@@ -204,19 +204,23 @@ class DevelopmentMenu:
|
|
|
204
204
|
complete_action()
|
|
205
205
|
return
|
|
206
206
|
if option == 1:
|
|
207
|
-
changed =
|
|
207
|
+
changed = self.git.get_all_modified_files() if self.git else []
|
|
208
208
|
self.linting.lint_modified(fix=False, changed_files=changed)
|
|
209
209
|
elif option == 2:
|
|
210
|
-
changed =
|
|
210
|
+
changed = self.git.get_all_modified_files() if self.git else []
|
|
211
211
|
self.linting.lint_modified(fix=True, changed_files=changed)
|
|
212
212
|
elif option == 3:
|
|
213
213
|
self.linting.lint_file(fix=False)
|
|
214
214
|
elif option == 4:
|
|
215
215
|
self.linting.lint_file(fix=True)
|
|
216
216
|
elif option == 5:
|
|
217
|
-
|
|
218
|
-
if
|
|
219
|
-
self.linting.
|
|
217
|
+
name = get_input_string("Enter model name or file path (e.g. stg_orders): ")
|
|
218
|
+
if name != "0":
|
|
219
|
+
path = name if name.endswith(".sql") else self.linting.resolve_model_path(name)
|
|
220
|
+
if path:
|
|
221
|
+
self.linting.lint_file(file_path=path, fix=False)
|
|
222
|
+
else:
|
|
223
|
+
error_line(f"Could not find model '{name}' in models/ directory")
|
|
220
224
|
elif option == 6:
|
|
221
225
|
if not self.downloads:
|
|
222
226
|
error_line("Download service not available")
|
|
@@ -42,6 +42,8 @@ class GitOperationsMenu:
|
|
|
42
42
|
self._cherry_pick_test()
|
|
43
43
|
elif option == 9:
|
|
44
44
|
self._deploy_prod()
|
|
45
|
+
elif option == 10:
|
|
46
|
+
self._reset_branch_from_main()
|
|
45
47
|
self._menu()
|
|
46
48
|
option = get_input_number("Choose an option: ")
|
|
47
49
|
|
|
@@ -57,6 +59,7 @@ class GitOperationsMenu:
|
|
|
57
59
|
menu_option(7, "squash to test - Squash merge into test")
|
|
58
60
|
menu_option(8, "remove from test - Remove a branch from test")
|
|
59
61
|
menu_option(9, "deploy to prod - Deploy main to production")
|
|
62
|
+
menu_option(10, "reset branch - Force reset a branch from main")
|
|
60
63
|
menu_option(0, "back - Return to main menu")
|
|
61
64
|
|
|
62
65
|
def _branch_menu(self) -> None:
|
|
@@ -190,7 +193,7 @@ class GitOperationsMenu:
|
|
|
190
193
|
display_action_header("Deploy to Environment")
|
|
191
194
|
|
|
192
195
|
# Build environment list from framework config, filtering non-deployable environments
|
|
193
|
-
excluded = {"unit-test", "ci", "local-dev", "prod", "test"}
|
|
196
|
+
excluded = {"unit-test", "ci", "local-dev", "prod", "test", "main"}
|
|
194
197
|
environments = {}
|
|
195
198
|
if self.source_control.environments:
|
|
196
199
|
for env in self.source_control.environments:
|
|
@@ -289,3 +292,39 @@ class GitOperationsMenu:
|
|
|
289
292
|
selected = commits[choice-1]
|
|
290
293
|
self.git.revert_from_test(selected["sha"], selected["branch"])
|
|
291
294
|
complete_action()
|
|
295
|
+
|
|
296
|
+
def _reset_branch_from_main(self) -> None:
|
|
297
|
+
display_action_header("Reset Branch from Main")
|
|
298
|
+
|
|
299
|
+
# Build list of resettable branches from environments + deployment_branches
|
|
300
|
+
excluded = {"main", "prod"}
|
|
301
|
+
branches = {}
|
|
302
|
+
if self.source_control.environments:
|
|
303
|
+
for env in self.source_control.environments:
|
|
304
|
+
if env.branch_name and env.branch_name.lower() not in excluded:
|
|
305
|
+
branches[env.branch_name] = env.branch_name
|
|
306
|
+
|
|
307
|
+
if not branches:
|
|
308
|
+
branches = {
|
|
309
|
+
k: v for k, v in self.deployment_branches.items()
|
|
310
|
+
if k.lower() not in excluded
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# Always include test if not already present
|
|
314
|
+
if "test" not in branches:
|
|
315
|
+
branches["test"] = "test"
|
|
316
|
+
|
|
317
|
+
if not branches:
|
|
318
|
+
error_line("No branches available to reset")
|
|
319
|
+
complete_action()
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
selected = select_from_dict(branches, "branch to reset")
|
|
323
|
+
if selected == "back":
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
warning_line(f"This will FORCE RESET '{selected}' to match origin/main")
|
|
327
|
+
warning_line("All existing commits on that branch will be lost")
|
|
328
|
+
if get_input_true_false("Are you sure?", "n"):
|
|
329
|
+
self.git.reset_branch_from_main(selected)
|
|
330
|
+
complete_action()
|
|
@@ -104,6 +104,8 @@ class DbtRunner:
|
|
|
104
104
|
args = [f"--target={self.target}"]
|
|
105
105
|
if select:
|
|
106
106
|
args += ["--select", select]
|
|
107
|
+
elif self.project_name:
|
|
108
|
+
args += ["--select", self.project_name]
|
|
107
109
|
if full_refresh:
|
|
108
110
|
args.append("--full-refresh")
|
|
109
111
|
return self._run_command("seed", args)
|
|
@@ -30,6 +30,22 @@ class GitService:
|
|
|
30
30
|
def get_new_files(self) -> list[GitCommitHelper]:
|
|
31
31
|
return [GitCommitHelper(file=f) for f in self.repo.untracked_files]
|
|
32
32
|
|
|
33
|
+
def get_all_modified_files(self) -> list[str]:
|
|
34
|
+
"""Get all files that differ from the last commit: unstaged, staged, and untracked."""
|
|
35
|
+
paths = set()
|
|
36
|
+
# Staged changes (index vs HEAD)
|
|
37
|
+
for d in self.repo.index.diff("HEAD"):
|
|
38
|
+
if d.change_type != "D" and d.a_path:
|
|
39
|
+
paths.add(d.a_path)
|
|
40
|
+
# Unstaged changes (working tree vs index)
|
|
41
|
+
for d in self.repo.index.diff(None):
|
|
42
|
+
if d.change_type != "D" and d.a_path:
|
|
43
|
+
paths.add(d.a_path)
|
|
44
|
+
# Untracked files
|
|
45
|
+
for f in self.repo.untracked_files:
|
|
46
|
+
paths.add(f)
|
|
47
|
+
return sorted(paths)
|
|
48
|
+
|
|
33
49
|
def get_local_branches(self) -> dict[str, str]:
|
|
34
50
|
return {b.name: b.name for b in self.repo.branches}
|
|
35
51
|
|
|
@@ -397,3 +413,13 @@ class GitService:
|
|
|
397
413
|
if branch.name not in ("main", "test"):
|
|
398
414
|
self.repo.delete_head(branch, force=True)
|
|
399
415
|
success_line("Reset to main completed")
|
|
416
|
+
|
|
417
|
+
def reset_branch_from_main(self, branch_name: str) -> None:
|
|
418
|
+
"""Force reset a remote branch to match origin/main."""
|
|
419
|
+
try:
|
|
420
|
+
self.repo.remotes.origin.fetch()
|
|
421
|
+
main_sha = self.repo.commit("origin/main").hexsha
|
|
422
|
+
self.repo.remotes.origin.push(refspec=f"{main_sha}:refs/heads/{branch_name}", force=True)
|
|
423
|
+
success_line(f"Reset '{branch_name}' to match main ({main_sha[:8]})")
|
|
424
|
+
except GitCommandError as e:
|
|
425
|
+
error_line(f"Failed to reset '{branch_name}': {e}")
|
{datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/linting_service.py
RENAMED
|
@@ -88,6 +88,21 @@ class LintingService:
|
|
|
88
88
|
warning_line(f"Missing packages: {', '.join(missing)}")
|
|
89
89
|
return self.install_requirements(missing)
|
|
90
90
|
|
|
91
|
+
def resolve_model_path(self, name: str) -> str | None:
|
|
92
|
+
"""Resolve a model name (e.g. 'stg_orders') to its SQL file path under models/."""
|
|
93
|
+
models_dir = self.project_dir / "models"
|
|
94
|
+
if not models_dir.exists():
|
|
95
|
+
return None
|
|
96
|
+
matches = list(models_dir.rglob(f"{name}.sql"))
|
|
97
|
+
if len(matches) == 1:
|
|
98
|
+
return str(matches[0].relative_to(self.project_dir))
|
|
99
|
+
if len(matches) > 1:
|
|
100
|
+
warning_line(f"Multiple matches for '{name}':")
|
|
101
|
+
for m in matches:
|
|
102
|
+
info_line(f" {m.relative_to(self.project_dir)}")
|
|
103
|
+
return None
|
|
104
|
+
return None
|
|
105
|
+
|
|
91
106
|
def lint_file(self, file_path: str = None, fix: bool = False) -> subprocess.CompletedProcess:
|
|
92
107
|
action = "fix" if fix else "lint"
|
|
93
108
|
cmd = ["sqlfluff", action]
|
|
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
|
|
File without changes
|
{datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/bootstrap_service.py
RENAMED
|
File without changes
|
{datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/dbt_project_generator.py
RENAMED
|
File without changes
|
{datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/directory_scaffolder.py
RENAMED
|
File without changes
|
{datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/download_service.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/src/datasecops_cli/services/snowflake_service.py
RENAMED
|
File without changes
|
{datasecops_cli-0.5.8 → datasecops_cli-0.5.10}/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
|
|
File without changes
|