datasecops-cli 0.2.6__tar.gz → 0.2.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.
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/CHANGELOG.md +25 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/PKG-INFO +54 -8
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/README.md +53 -7
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/docs/getting-started.md +1 -1
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/pyproject.toml +1 -1
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/setup.ps1 +6 -5
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/setup.sh +4 -4
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/main.py +113 -48
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/menus/development.py +64 -23
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/menus/downloads.py +6 -3
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/services/bootstrap_service.py +12 -8
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/services/dbt_runner.py +5 -2
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/services/download_service.py +36 -13
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/services/linting_service.py +13 -7
- datasecops_cli-0.2.8/tests/test_main.py +207 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/.github/workflows/publish-cli.yml +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/.gitignore +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/DEVELOPMENT.md +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/LICENSE +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/docs/legacy.md +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/docs/legacy_plan_of_action.md +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/docs/mcp-server.md +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/mcp-servers.json +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/__init__.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/config.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/menus/__init__.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/menus/git_operations.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/models/__init__.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/models/git_helpers.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/models/project_config.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/services/__init__.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/services/git_service.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/services/skill_service.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/services/snowflake_service.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/utilities/__init__.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/utilities/display.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/utilities/file_utils.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_mcp/__init__.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_mcp/__main__.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_mcp/connection.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_mcp/server.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/tests/__init__.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/tests/test_config.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/tests/test_file_utils.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/tests/test_models.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/tests/test_version.py +0 -0
- {datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/tests/test_yaml_utils.py +0 -0
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the DataSecOps CLI are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.8] - 2026-05-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Non-interactive `download` subcommand** — `datasecops download <items>` downloads framework config without prompts, designed for CI/CD pipelines. Supports `sqlfluff`, `pipelines`, `packages`, `macros`, or `all`
|
|
10
|
+
- **Runtime `dbtf` check** — the CLI now warns at startup if dbt Fusion (`dbtf`) is not found on PATH
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Platform auto-detected from framework config** — pipeline downloads, bootstrap, and the downloads menu now read `source_control_platform` from the native app instead of prompting the user to select GitHub or Azure DevOps
|
|
15
|
+
- **Case-insensitive platform matching** — `download_pipelines()` now compares platform values case-insensitively, fixing a bug where pipelines stored as `"GitHub"` would not match the lowercase `"github"` filter
|
|
16
|
+
- **All dbt commands use `dbtf`** — `DbtRunner` and setup scripts now consistently use the `dbtf` binary (dbt Fusion) instead of `dbt`
|
|
17
|
+
- **Refactored CLI entry point** — `main()` now uses `argparse` with subcommands (`bootstrap`, `download`) instead of manual `sys.argv` parsing. Shared connection/config logic extracted to `_connect_and_load()`
|
|
18
|
+
- **Development menu option 14** relabelled to clarify it installs dbt-core/dbt-snowflake for linting, not dbt Fusion
|
|
19
|
+
|
|
20
|
+
## [0.2.7] - 2026-05-12
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Separate SQLFluff and dbt installs** — `get_sqlfluff_requirements()` now returns only `sqlfluff` and `sqlfluff-templater-dbt`; new `get_dbt_requirements()` returns only `dbt-core` and `dbt-snowflake`. The lint menu install option and the new development menu install option each handle their own packages independently
|
|
25
|
+
- **Reorganised development menu** — added `[7] parse` for `dbt parse`, added `[14] install dbt` for explicit dbt-core/dbt-snowflake installation, removed standalone retry (available in run and test submenus)
|
|
26
|
+
- **Expanded test submenu** — added unit tests (`test_type:unit`), data tests (`test_type:data`), and failed test retry options alongside all tests and specific selector
|
|
27
|
+
- **Clearer input prompts** — run, test, and lint specific-file prompts now include examples of valid input (e.g. `my_model+`, `tag:nightly`, `models/staging/stg_orders.sql`)
|
|
28
|
+
- **Bootstrap shows separate install hints** — step 7 now lists SQLFluff and dbt requirements separately with individual `uv pip install` commands
|
|
29
|
+
|
|
5
30
|
## [0.2.6] - 2026-05-11
|
|
6
31
|
|
|
7
32
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datasecops-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: DataSecOps Framework CLI for Snowflake Native App
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -55,7 +55,7 @@ Requires Python 3.10 or later.
|
|
|
55
55
|
|
|
56
56
|
Optional:
|
|
57
57
|
|
|
58
|
-
- **dbt Fusion**
|
|
58
|
+
- **dbt Fusion** — required for dbt commands (`dbtf`). Install from https://docs.getdbt.com/docs/core/installation
|
|
59
59
|
- **Cortex Code** for skill downloads
|
|
60
60
|
- **Node.js 18+** for GitHub/Azure DevOps MCP servers
|
|
61
61
|
|
|
@@ -96,6 +96,58 @@ datasecops
|
|
|
96
96
|
| **Git** | Branch create/checkout/delete, commit & push, rebase, squash, deploy to environment branches, cherry-pick |
|
|
97
97
|
| **Downloads** | SQLFluff config, CI/CD pipelines (GitHub Actions / Azure DevOps), dbt packages, Cortex Code skills |
|
|
98
98
|
|
|
99
|
+
## Non-Interactive Mode (CI/CD)
|
|
100
|
+
|
|
101
|
+
The `download` subcommand lets you pull framework config in CI/CD pipelines without interactive prompts:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Download specific items
|
|
105
|
+
datasecops download sqlfluff
|
|
106
|
+
datasecops download sqlfluff packages
|
|
107
|
+
datasecops download pipelines macros
|
|
108
|
+
|
|
109
|
+
# Download everything
|
|
110
|
+
datasecops download all
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Available items: `sqlfluff`, `pipelines`, `packages`, `macros`, `all`
|
|
114
|
+
|
|
115
|
+
The pipeline platform (GitHub / Azure DevOps) is auto-detected from the native app's source control configuration.
|
|
116
|
+
|
|
117
|
+
### Pipeline Setup
|
|
118
|
+
|
|
119
|
+
Your pipeline needs two things:
|
|
120
|
+
|
|
121
|
+
1. **A `.datasecops.yml`** in the repo (already committed — contains no secrets):
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
connection_name: "ci"
|
|
125
|
+
app_database: "DATA_ENGINEERS_DATASECOPS_FRAMEWORK"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
2. **A Snowflake connection** in `~/.snowflake/connections.toml` for the CI service account:
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
# GitHub Actions example
|
|
132
|
+
- name: Configure Snowflake connection
|
|
133
|
+
run: |
|
|
134
|
+
mkdir -p ~/.snowflake
|
|
135
|
+
cat > ~/.snowflake/connections.toml << EOF
|
|
136
|
+
[ci]
|
|
137
|
+
account = "${{ vars.SNOWFLAKE_ACCOUNT }}"
|
|
138
|
+
user = "${{ vars.SNOWFLAKE_USER }}"
|
|
139
|
+
authenticator = "snowflake_jwt"
|
|
140
|
+
private_key_file = "/tmp/rsa_key.p8"
|
|
141
|
+
warehouse = "CI_WH"
|
|
142
|
+
role = "CI_ROLE"
|
|
143
|
+
EOF
|
|
144
|
+
|
|
145
|
+
- name: Download SQLFluff config
|
|
146
|
+
run: datasecops download sqlfluff
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The exit code is `0` on success, `1` if any download fails.
|
|
150
|
+
|
|
99
151
|
## MCP Server
|
|
100
152
|
|
|
101
153
|
The package includes an MCP (Model Context Protocol) server that exposes your framework's governance configuration to AI coding assistants. Instead of static skill files, the MCP server gives AI tools live access to your native app's current rules.
|
|
@@ -154,12 +206,6 @@ app_database: "DATA_ENGINEERS_DATASECOPS_FRAMEWORK"
|
|
|
154
206
|
|
|
155
207
|
Project profiles, linting rules, pipeline templates, and deployment targets are all managed centrally in the native app and pulled down by the CLI.
|
|
156
208
|
|
|
157
|
-
## Documentation
|
|
158
|
-
|
|
159
|
-
- [Getting Started Guide](docs/getting-started.md) — install CLI, configure MCP servers for VS Code/Cursor/Cortex Code with Snowflake, dbt, and GitHub/Azure DevOps
|
|
160
|
-
- [MCP Server Reference](docs/mcp-server.md) — full tool documentation, architecture, and usage examples
|
|
161
|
-
- [Development Guide](DEVELOPMENT.md) — project structure, setup scripts, native app API reference, and publishing details
|
|
162
|
-
|
|
163
209
|
## License
|
|
164
210
|
|
|
165
211
|
MIT
|
|
@@ -35,7 +35,7 @@ Requires Python 3.10 or later.
|
|
|
35
35
|
|
|
36
36
|
Optional:
|
|
37
37
|
|
|
38
|
-
- **dbt Fusion**
|
|
38
|
+
- **dbt Fusion** — required for dbt commands (`dbtf`). Install from https://docs.getdbt.com/docs/core/installation
|
|
39
39
|
- **Cortex Code** for skill downloads
|
|
40
40
|
- **Node.js 18+** for GitHub/Azure DevOps MCP servers
|
|
41
41
|
|
|
@@ -76,6 +76,58 @@ datasecops
|
|
|
76
76
|
| **Git** | Branch create/checkout/delete, commit & push, rebase, squash, deploy to environment branches, cherry-pick |
|
|
77
77
|
| **Downloads** | SQLFluff config, CI/CD pipelines (GitHub Actions / Azure DevOps), dbt packages, Cortex Code skills |
|
|
78
78
|
|
|
79
|
+
## Non-Interactive Mode (CI/CD)
|
|
80
|
+
|
|
81
|
+
The `download` subcommand lets you pull framework config in CI/CD pipelines without interactive prompts:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Download specific items
|
|
85
|
+
datasecops download sqlfluff
|
|
86
|
+
datasecops download sqlfluff packages
|
|
87
|
+
datasecops download pipelines macros
|
|
88
|
+
|
|
89
|
+
# Download everything
|
|
90
|
+
datasecops download all
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Available items: `sqlfluff`, `pipelines`, `packages`, `macros`, `all`
|
|
94
|
+
|
|
95
|
+
The pipeline platform (GitHub / Azure DevOps) is auto-detected from the native app's source control configuration.
|
|
96
|
+
|
|
97
|
+
### Pipeline Setup
|
|
98
|
+
|
|
99
|
+
Your pipeline needs two things:
|
|
100
|
+
|
|
101
|
+
1. **A `.datasecops.yml`** in the repo (already committed — contains no secrets):
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
connection_name: "ci"
|
|
105
|
+
app_database: "DATA_ENGINEERS_DATASECOPS_FRAMEWORK"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
2. **A Snowflake connection** in `~/.snowflake/connections.toml` for the CI service account:
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
# GitHub Actions example
|
|
112
|
+
- name: Configure Snowflake connection
|
|
113
|
+
run: |
|
|
114
|
+
mkdir -p ~/.snowflake
|
|
115
|
+
cat > ~/.snowflake/connections.toml << EOF
|
|
116
|
+
[ci]
|
|
117
|
+
account = "${{ vars.SNOWFLAKE_ACCOUNT }}"
|
|
118
|
+
user = "${{ vars.SNOWFLAKE_USER }}"
|
|
119
|
+
authenticator = "snowflake_jwt"
|
|
120
|
+
private_key_file = "/tmp/rsa_key.p8"
|
|
121
|
+
warehouse = "CI_WH"
|
|
122
|
+
role = "CI_ROLE"
|
|
123
|
+
EOF
|
|
124
|
+
|
|
125
|
+
- name: Download SQLFluff config
|
|
126
|
+
run: datasecops download sqlfluff
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The exit code is `0` on success, `1` if any download fails.
|
|
130
|
+
|
|
79
131
|
## MCP Server
|
|
80
132
|
|
|
81
133
|
The package includes an MCP (Model Context Protocol) server that exposes your framework's governance configuration to AI coding assistants. Instead of static skill files, the MCP server gives AI tools live access to your native app's current rules.
|
|
@@ -134,12 +186,6 @@ app_database: "DATA_ENGINEERS_DATASECOPS_FRAMEWORK"
|
|
|
134
186
|
|
|
135
187
|
Project profiles, linting rules, pipeline templates, and deployment targets are all managed centrally in the native app and pulled down by the CLI.
|
|
136
188
|
|
|
137
|
-
## Documentation
|
|
138
|
-
|
|
139
|
-
- [Getting Started Guide](docs/getting-started.md) — install CLI, configure MCP servers for VS Code/Cursor/Cortex Code with Snowflake, dbt, and GitHub/Azure DevOps
|
|
140
|
-
- [MCP Server Reference](docs/mcp-server.md) — full tool documentation, architecture, and usage examples
|
|
141
|
-
- [Development Guide](DEVELOPMENT.md) — project structure, setup scripts, native app API reference, and publishing details
|
|
142
|
-
|
|
143
189
|
## License
|
|
144
190
|
|
|
145
191
|
MIT
|
|
@@ -14,7 +14,7 @@ Before you begin, ensure you have:
|
|
|
14
14
|
- **Git** configured with access to your repository
|
|
15
15
|
|
|
16
16
|
Optional:
|
|
17
|
-
- **dbt Fusion**
|
|
17
|
+
- **dbt Fusion** — required for dbt commands (`dbtf`). Install from https://docs.getdbt.com/docs/core/installation. The CLI invokes `dbtf` for all dbt operations.
|
|
18
18
|
- **A GitHub Personal Access Token** (for GitHub MCP server)
|
|
19
19
|
- **An Azure DevOps PAT** (for Azure DevOps MCP server)
|
|
20
20
|
|
|
@@ -16,13 +16,14 @@ try {
|
|
|
16
16
|
$env:Path = "$env:USERPROFILE\.local\bin;$env:Path"
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
# Check for dbt
|
|
19
|
+
# Check for dbt Fusion (dbtf)
|
|
20
20
|
try {
|
|
21
|
-
$null = Get-Command
|
|
22
|
-
Write-Host "[OK] dbt found" -ForegroundColor Green
|
|
21
|
+
$null = Get-Command dbtf -ErrorAction Stop
|
|
22
|
+
Write-Host "[OK] dbt Fusion (dbtf) found" -ForegroundColor Green
|
|
23
23
|
} catch {
|
|
24
|
-
Write-Host "WARNING: dbt not found on PATH." -ForegroundColor Yellow
|
|
25
|
-
Write-Host " Install dbt Fusion
|
|
24
|
+
Write-Host "WARNING: dbt Fusion (dbtf) not found on PATH." -ForegroundColor Yellow
|
|
25
|
+
Write-Host " Install dbt Fusion: https://docs.getdbt.com/docs/core/installation" -ForegroundColor Yellow
|
|
26
|
+
Write-Host " The CLI uses 'dbtf' for all dbt commands." -ForegroundColor Yellow
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
# Check for cortex (optional)
|
|
@@ -16,12 +16,12 @@ if ! command -v uv &> /dev/null; then
|
|
|
16
16
|
fi
|
|
17
17
|
echo "[OK] uv $(uv --version 2>/dev/null || echo 'installed')"
|
|
18
18
|
|
|
19
|
-
# Check for dbt
|
|
20
|
-
if ! command -v
|
|
19
|
+
# Check for dbt Fusion (dbtf)
|
|
20
|
+
if ! command -v dbtf &> /dev/null; then
|
|
21
21
|
echo ""
|
|
22
|
-
echo "WARNING: dbt not found on PATH."
|
|
22
|
+
echo "WARNING: dbt Fusion (dbtf) not found on PATH."
|
|
23
23
|
echo " Install dbt Fusion: https://docs.getdbt.com/docs/core/installation"
|
|
24
|
-
echo "
|
|
24
|
+
echo " The CLI uses 'dbtf' for all dbt commands."
|
|
25
25
|
echo ""
|
|
26
26
|
fi
|
|
27
27
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import shutil
|
|
1
3
|
import sys
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
|
|
@@ -18,22 +20,55 @@ from datasecops_cli.utilities.display import (
|
|
|
18
20
|
)
|
|
19
21
|
|
|
20
22
|
|
|
23
|
+
DOWNLOAD_ITEMS = ["sqlfluff", "pipelines", "packages", "macros", "all"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
27
|
+
parser = argparse.ArgumentParser(
|
|
28
|
+
prog="datasecops",
|
|
29
|
+
description="DataSecOps Framework CLI for Snowflake Native App",
|
|
30
|
+
)
|
|
31
|
+
sub = parser.add_subparsers(dest="command")
|
|
32
|
+
|
|
33
|
+
sub.add_parser("bootstrap", help="Set up a new dbt project with all framework config")
|
|
34
|
+
|
|
35
|
+
dl = sub.add_parser("download", help="Download framework config non-interactively (for CI/CD)")
|
|
36
|
+
dl.add_argument(
|
|
37
|
+
"items",
|
|
38
|
+
nargs="+",
|
|
39
|
+
choices=DOWNLOAD_ITEMS,
|
|
40
|
+
help="Item(s) to download: sqlfluff, pipelines, packages, macros, or all",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return parser
|
|
44
|
+
|
|
45
|
+
|
|
21
46
|
def main():
|
|
22
47
|
"""Main entry point for the datasecops CLI."""
|
|
48
|
+
parser = _build_parser()
|
|
49
|
+
args = parser.parse_args()
|
|
50
|
+
|
|
23
51
|
config = Config()
|
|
24
52
|
|
|
25
|
-
|
|
26
|
-
if len(sys.argv) > 1 and sys.argv[1] == "bootstrap":
|
|
53
|
+
if args.command == "bootstrap":
|
|
27
54
|
_run_bootstrap(config)
|
|
28
|
-
|
|
55
|
+
elif args.command == "download":
|
|
56
|
+
_run_download(config, args.items)
|
|
57
|
+
else:
|
|
58
|
+
_run_interactive(config)
|
|
29
59
|
|
|
60
|
+
|
|
61
|
+
def _connect_and_load(config: Config) -> SnowflakeService:
|
|
62
|
+
"""Load config, connect to Snowflake, and load native app settings.
|
|
63
|
+
|
|
64
|
+
Returns the connected SnowflakeService, or calls sys.exit on failure.
|
|
65
|
+
"""
|
|
30
66
|
if not config.load():
|
|
31
67
|
sys.exit(1)
|
|
32
|
-
|
|
33
|
-
# Connect to Snowflake
|
|
68
|
+
|
|
34
69
|
sf_config = config.datasecops
|
|
35
70
|
sf_service = SnowflakeService(sf_config)
|
|
36
|
-
|
|
71
|
+
|
|
37
72
|
try:
|
|
38
73
|
info_line(f"Connecting to Snowflake ({sf_config.connection_name})...")
|
|
39
74
|
sf_service.connect()
|
|
@@ -41,38 +76,85 @@ def main():
|
|
|
41
76
|
except Exception as e:
|
|
42
77
|
error_line(f"Failed to connect to Snowflake: {e}")
|
|
43
78
|
sys.exit(1)
|
|
44
|
-
|
|
79
|
+
|
|
80
|
+
info_line("Loading framework configuration...")
|
|
81
|
+
config.load_from_native_app(sf_service)
|
|
82
|
+
|
|
83
|
+
if not config.profile:
|
|
84
|
+
error_line(f"Profile '{config.profile_name}' not found in native app")
|
|
85
|
+
sf_service.close()
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
success_line(f"Profile: {config.profile.project_name} ({config.profile_name})")
|
|
89
|
+
return sf_service
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _run_interactive(config: Config):
|
|
93
|
+
"""Run the interactive menu-driven CLI."""
|
|
94
|
+
sf_service = _connect_and_load(config)
|
|
95
|
+
|
|
45
96
|
try:
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
sys.exit(1)
|
|
53
|
-
|
|
54
|
-
success_line(f"Profile: {config.profile.project_name} ({config.profile_name})")
|
|
55
|
-
|
|
97
|
+
# Check for dbt Fusion (dbtf) on PATH
|
|
98
|
+
if not shutil.which("dbtf"):
|
|
99
|
+
from datasecops_cli.utilities.display import warning_line
|
|
100
|
+
warning_line("dbt Fusion (dbtf) not found on PATH — dbt commands will not work.")
|
|
101
|
+
warning_line("Install dbt Fusion: https://docs.getdbt.com/docs/core/installation")
|
|
102
|
+
|
|
56
103
|
# Initialize services
|
|
57
104
|
dbt_runner = DbtRunner(
|
|
58
105
|
project_dir=config.dbt_project_dir,
|
|
59
106
|
profiles_dir=config.get_dbt_profiles_dir(),
|
|
60
107
|
target=config.project_settings.get_default_target().target_name if config.get_default_target() else "dev"
|
|
61
108
|
)
|
|
62
|
-
|
|
109
|
+
|
|
63
110
|
try:
|
|
64
111
|
git_service = GitService(config.project_dir)
|
|
65
112
|
except Exception:
|
|
66
113
|
git_service = None
|
|
67
|
-
|
|
114
|
+
|
|
68
115
|
linting_service = LintingService(config.dbt_project_dir)
|
|
69
116
|
download_service = DownloadService(sf_service, config.project_dir)
|
|
70
117
|
skill_service = SkillService(sf_service)
|
|
71
|
-
|
|
118
|
+
|
|
72
119
|
# Main menu loop
|
|
73
|
-
_main_menu(config, dbt_runner, git_service, linting_service,
|
|
120
|
+
_main_menu(config, dbt_runner, git_service, linting_service,
|
|
74
121
|
download_service, skill_service, sf_service)
|
|
75
|
-
|
|
122
|
+
finally:
|
|
123
|
+
sf_service.close()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _run_download(config: Config, items: list[str]):
|
|
127
|
+
"""Run non-interactive downloads for CI/CD pipelines."""
|
|
128
|
+
sf_service = _connect_and_load(config)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
download_service = DownloadService(sf_service, config.project_dir)
|
|
132
|
+
profiles_dir = str(config.get_dbt_profiles_dir())
|
|
133
|
+
|
|
134
|
+
if "all" in items:
|
|
135
|
+
items = ["sqlfluff", "pipelines", "packages", "macros"]
|
|
136
|
+
|
|
137
|
+
failed = False
|
|
138
|
+
for item in items:
|
|
139
|
+
info_line("")
|
|
140
|
+
if item == "sqlfluff":
|
|
141
|
+
if not download_service.download_sqlfluff_config(
|
|
142
|
+
profiles_dir=profiles_dir, dbt_project_dir=config.dbt_project_dir
|
|
143
|
+
):
|
|
144
|
+
failed = True
|
|
145
|
+
elif item == "pipelines":
|
|
146
|
+
platform = config.source_control.source_control_platform.lower()
|
|
147
|
+
info_line(f"Platform: {platform}")
|
|
148
|
+
if not download_service.download_pipelines(platform=platform):
|
|
149
|
+
failed = True
|
|
150
|
+
elif item == "packages":
|
|
151
|
+
if not download_service.download_dbt_packages(config.dbt_project_dir):
|
|
152
|
+
failed = True
|
|
153
|
+
elif item == "macros":
|
|
154
|
+
if not download_service.download_macros(config.profile_name, config.dbt_project_dir):
|
|
155
|
+
failed = True
|
|
156
|
+
|
|
157
|
+
sys.exit(1 if failed else 0)
|
|
76
158
|
finally:
|
|
77
159
|
sf_service.close()
|
|
78
160
|
|
|
@@ -110,12 +192,14 @@ def _main_menu(config: Config, dbt_runner: DbtRunner, git_service: GitService,
|
|
|
110
192
|
profile_name, config.dbt_project_dir,
|
|
111
193
|
project_settings=config.project_settings,
|
|
112
194
|
profile=config.profile,
|
|
195
|
+
source_control=config.source_control,
|
|
113
196
|
)
|
|
114
197
|
dl_menu.show()
|
|
115
198
|
|
|
116
199
|
elif option == 4:
|
|
117
|
-
from datasecops_cli.utilities.display import
|
|
118
|
-
platform =
|
|
200
|
+
from datasecops_cli.utilities.display import get_input_true_false
|
|
201
|
+
platform = config.source_control.source_control_platform.lower()
|
|
202
|
+
info_line(f"Platform: {platform}")
|
|
119
203
|
install_skills = get_input_true_false("Install Cortex Code skills?")
|
|
120
204
|
run_deps = get_input_true_false("Run dbt deps after downloading packages?")
|
|
121
205
|
bootstrap = BootstrapService(
|
|
@@ -145,35 +229,16 @@ def _show_main_menu(profile_name: str, git_service: GitService = None):
|
|
|
145
229
|
|
|
146
230
|
def _run_bootstrap(config: Config):
|
|
147
231
|
"""Run the bootstrap command to initialise a new project."""
|
|
148
|
-
from datasecops_cli.utilities.display import
|
|
232
|
+
from datasecops_cli.utilities.display import get_input_true_false
|
|
149
233
|
|
|
150
|
-
|
|
151
|
-
sys.exit(1)
|
|
152
|
-
|
|
153
|
-
sf_config = config.datasecops
|
|
154
|
-
sf_service = SnowflakeService(sf_config)
|
|
155
|
-
|
|
156
|
-
try:
|
|
157
|
-
info_line(f"Connecting to Snowflake ({sf_config.connection_name})...")
|
|
158
|
-
sf_service.connect()
|
|
159
|
-
success_line("Connected")
|
|
160
|
-
except Exception as e:
|
|
161
|
-
error_line(f"Failed to connect to Snowflake: {e}")
|
|
162
|
-
sys.exit(1)
|
|
234
|
+
sf_service = _connect_and_load(config)
|
|
163
235
|
|
|
164
236
|
try:
|
|
165
|
-
info_line("Loading framework configuration...")
|
|
166
|
-
config.load_from_native_app(sf_service)
|
|
167
|
-
|
|
168
|
-
if not config.profile:
|
|
169
|
-
error_line(f"Profile '{config.profile_name}' not found in native app")
|
|
170
|
-
sys.exit(1)
|
|
171
|
-
|
|
172
|
-
success_line(f"Profile: {config.profile.project_name} ({config.profile_name})")
|
|
173
237
|
info_line("")
|
|
174
238
|
|
|
175
|
-
#
|
|
176
|
-
platform =
|
|
239
|
+
# Use platform from framework config
|
|
240
|
+
platform = config.source_control.source_control_platform.lower()
|
|
241
|
+
info_line(f"Platform: {platform}")
|
|
177
242
|
|
|
178
243
|
# Ask about optional steps
|
|
179
244
|
install_skills = get_input_true_false("Install Cortex Code skills?")
|
|
@@ -48,33 +48,35 @@ class DevelopmentMenu:
|
|
|
48
48
|
self.dbt.deps()
|
|
49
49
|
complete_action()
|
|
50
50
|
elif option == 7:
|
|
51
|
+
display_action_header("dbt parse")
|
|
52
|
+
self.dbt.parse()
|
|
53
|
+
complete_action()
|
|
54
|
+
elif option == 8:
|
|
51
55
|
display_action_header("dbt compile")
|
|
52
56
|
self.dbt.compile()
|
|
53
57
|
complete_action()
|
|
54
|
-
elif option ==
|
|
58
|
+
elif option == 9:
|
|
55
59
|
display_action_header("dbt snapshot")
|
|
56
60
|
self.dbt.snapshot()
|
|
57
61
|
complete_action()
|
|
58
|
-
elif option ==
|
|
62
|
+
elif option == 10:
|
|
59
63
|
display_action_header("dbt source freshness")
|
|
60
64
|
self.dbt.source_freshness()
|
|
61
65
|
complete_action()
|
|
62
|
-
elif option ==
|
|
66
|
+
elif option == 11:
|
|
63
67
|
display_action_header("dbt clean")
|
|
64
68
|
self.dbt.clean()
|
|
65
69
|
complete_action()
|
|
66
|
-
elif option ==
|
|
70
|
+
elif option == 12:
|
|
67
71
|
display_action_header("dbt debug")
|
|
68
72
|
self.dbt.debug()
|
|
69
73
|
complete_action()
|
|
70
|
-
elif option ==
|
|
74
|
+
elif option == 13:
|
|
71
75
|
display_action_header("dbt list")
|
|
72
76
|
self.dbt.list_models()
|
|
73
77
|
complete_action()
|
|
74
|
-
elif option ==
|
|
75
|
-
|
|
76
|
-
self.dbt.retry()
|
|
77
|
-
complete_action()
|
|
78
|
+
elif option == 14:
|
|
79
|
+
self._install_dbt_requirements()
|
|
78
80
|
self._menu()
|
|
79
81
|
option = get_input_number("Choose an option: ")
|
|
80
82
|
|
|
@@ -87,13 +89,14 @@ class DevelopmentMenu:
|
|
|
87
89
|
menu_option(4, "docs - Generate & serve dbt docs")
|
|
88
90
|
menu_option(5, "seed - Load seed data")
|
|
89
91
|
menu_option(6, "deps - Install dbt packages")
|
|
90
|
-
menu_option(7, "
|
|
91
|
-
menu_option(8, "
|
|
92
|
-
menu_option(9, "
|
|
93
|
-
menu_option(10, "
|
|
94
|
-
menu_option(11, "
|
|
95
|
-
menu_option(12, "
|
|
96
|
-
menu_option(13, "
|
|
92
|
+
menu_option(7, "parse - Parse dbt project")
|
|
93
|
+
menu_option(8, "compile - Compile dbt models")
|
|
94
|
+
menu_option(9, "snapshot - Run dbt snapshots")
|
|
95
|
+
menu_option(10, "freshness - Check source freshness")
|
|
96
|
+
menu_option(11, "clean - Clean dbt target")
|
|
97
|
+
menu_option(12, "debug - Debug dbt connection")
|
|
98
|
+
menu_option(13, "list - List dbt resources")
|
|
99
|
+
menu_option(14, "install lint - Install dbt-core & dbt-snowflake for linting (not dbt Fusion)")
|
|
97
100
|
menu_option(0, "back - Return to main menu")
|
|
98
101
|
|
|
99
102
|
def _run_menu(self) -> None:
|
|
@@ -113,7 +116,7 @@ class DevelopmentMenu:
|
|
|
113
116
|
full_refresh = get_input_true_false("Full refresh?", "n")
|
|
114
117
|
self.dbt.run(modified_only=True, full_refresh=full_refresh)
|
|
115
118
|
elif option == 3:
|
|
116
|
-
select = get_input_string("Enter model selector: ")
|
|
119
|
+
select = get_input_string("Enter model selector (e.g. my_model, my_model+, tag:nightly, path:models/...): ")
|
|
117
120
|
if select != "0":
|
|
118
121
|
self.dbt.run(select=select)
|
|
119
122
|
elif option == 4:
|
|
@@ -129,13 +132,22 @@ class DevelopmentMenu:
|
|
|
129
132
|
clear()
|
|
130
133
|
display_action_header("dbt Test Options")
|
|
131
134
|
menu_option(1, "all tests - Run all tests")
|
|
132
|
-
menu_option(2, "
|
|
135
|
+
menu_option(2, "unit tests - Run unit tests only")
|
|
136
|
+
menu_option(3, "data tests - Run data tests only")
|
|
137
|
+
menu_option(4, "failed - Retry failed tests")
|
|
138
|
+
menu_option(5, "specific - Run specific test(s)")
|
|
133
139
|
menu_option(0, "back - Return to development menu")
|
|
134
140
|
option = get_input_number("Choose an option: ")
|
|
135
141
|
if option == 1:
|
|
136
142
|
self.dbt.test()
|
|
137
143
|
elif option == 2:
|
|
138
|
-
select
|
|
144
|
+
self.dbt.test(select="test_type:unit")
|
|
145
|
+
elif option == 3:
|
|
146
|
+
self.dbt.test(select="test_type:data")
|
|
147
|
+
elif option == 4:
|
|
148
|
+
self.dbt.retry()
|
|
149
|
+
elif option == 5:
|
|
150
|
+
select = get_input_string("Enter test selector (e.g. test_name, model_name, tag:nightly): ")
|
|
139
151
|
if select != "0":
|
|
140
152
|
self.dbt.test(select=select)
|
|
141
153
|
complete_action()
|
|
@@ -179,7 +191,7 @@ class DevelopmentMenu:
|
|
|
179
191
|
elif option == 4:
|
|
180
192
|
self.linting.lint_file(fix=True)
|
|
181
193
|
elif option == 5:
|
|
182
|
-
path = get_input_string("Enter file path: ")
|
|
194
|
+
path = get_input_string("Enter SQL file path (e.g. models/staging/stg_orders.sql): ")
|
|
183
195
|
if path != "0":
|
|
184
196
|
self.linting.lint_file(file_path=path, fix=False)
|
|
185
197
|
elif option == 6:
|
|
@@ -192,11 +204,11 @@ class DevelopmentMenu:
|
|
|
192
204
|
error_line("Download service not available")
|
|
193
205
|
return
|
|
194
206
|
|
|
195
|
-
# Show current versions
|
|
207
|
+
# Show current sqlfluff versions
|
|
196
208
|
installed = self.linting.get_installed_versions()
|
|
197
209
|
info_line("Currently installed:")
|
|
198
|
-
for pkg
|
|
199
|
-
info_line(f" {pkg}: {
|
|
210
|
+
for pkg in ("sqlfluff", "sqlfluff-templater-dbt"):
|
|
211
|
+
info_line(f" {pkg}: {installed.get(pkg) or 'not installed'}")
|
|
200
212
|
|
|
201
213
|
# Fetch required versions from framework
|
|
202
214
|
packages = self.downloads.get_sqlfluff_requirements()
|
|
@@ -210,3 +222,32 @@ class DevelopmentMenu:
|
|
|
210
222
|
|
|
211
223
|
info_line("")
|
|
212
224
|
self.linting.install_requirements(packages)
|
|
225
|
+
|
|
226
|
+
def _install_dbt_requirements(self) -> None:
|
|
227
|
+
"""Install dbt-core and dbt-snowflake at versions defined by the framework."""
|
|
228
|
+
display_action_header("Install dbt Requirements")
|
|
229
|
+
if not self.downloads:
|
|
230
|
+
error_line("Download service not available")
|
|
231
|
+
complete_action()
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
# Show current dbt versions
|
|
235
|
+
installed = self.linting.get_installed_versions()
|
|
236
|
+
info_line("Currently installed:")
|
|
237
|
+
for pkg in ("dbt-core", "dbt-snowflake"):
|
|
238
|
+
info_line(f" {pkg}: {installed.get(pkg) or 'not installed'}")
|
|
239
|
+
|
|
240
|
+
# Fetch required dbt versions from framework
|
|
241
|
+
packages = self.downloads.get_dbt_requirements()
|
|
242
|
+
if not packages:
|
|
243
|
+
complete_action()
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
info_line("")
|
|
247
|
+
info_line("Framework requires:")
|
|
248
|
+
for pkg in packages:
|
|
249
|
+
info_line(f" {pkg}")
|
|
250
|
+
|
|
251
|
+
info_line("")
|
|
252
|
+
self.linting.install_requirements(packages)
|
|
253
|
+
complete_action()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
from datasecops_cli.models.project_config import ProjectProfile, ProjectSettings
|
|
3
|
+
from datasecops_cli.models.project_config import ProjectProfile, ProjectSettings, SourceControl
|
|
4
4
|
from datasecops_cli.services.download_service import DownloadService
|
|
5
5
|
from datasecops_cli.services.skill_service import SkillService
|
|
6
6
|
from datasecops_cli.services.dbt_runner import DbtRunner
|
|
@@ -14,7 +14,8 @@ from datasecops_cli.utilities.display import (
|
|
|
14
14
|
class DownloadsMenu:
|
|
15
15
|
def __init__(self, download_service: DownloadService, skill_service: SkillService,
|
|
16
16
|
dbt_runner: DbtRunner, profile_name: str, dbt_project_dir: Path,
|
|
17
|
-
project_settings: ProjectSettings = None, profile: ProjectProfile = None
|
|
17
|
+
project_settings: ProjectSettings = None, profile: ProjectProfile = None,
|
|
18
|
+
source_control: SourceControl = None):
|
|
18
19
|
self.downloads = download_service
|
|
19
20
|
self.skills = skill_service
|
|
20
21
|
self.dbt = dbt_runner
|
|
@@ -22,6 +23,7 @@ class DownloadsMenu:
|
|
|
22
23
|
self.dbt_project_dir = dbt_project_dir
|
|
23
24
|
self.project_settings = project_settings
|
|
24
25
|
self.profile = profile
|
|
26
|
+
self.source_control = source_control
|
|
25
27
|
|
|
26
28
|
def show(self) -> None:
|
|
27
29
|
self._menu()
|
|
@@ -34,7 +36,8 @@ class DownloadsMenu:
|
|
|
34
36
|
complete_action()
|
|
35
37
|
elif option == 2:
|
|
36
38
|
display_action_header("Download Pipeline Files")
|
|
37
|
-
platform =
|
|
39
|
+
platform = self.source_control.source_control_platform.lower() if self.source_control else "github"
|
|
40
|
+
info_line(f"Platform: {platform}")
|
|
38
41
|
self.downloads.download_pipelines(platform=platform)
|
|
39
42
|
complete_action()
|
|
40
43
|
elif option == 3:
|
{datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/services/bootstrap_service.py
RENAMED
|
@@ -109,14 +109,18 @@ class BootstrapService:
|
|
|
109
109
|
info_line(" (dbt deps failed - run manually after fixing packages.yml)")
|
|
110
110
|
steps_passed += 1
|
|
111
111
|
|
|
112
|
-
# Step 7:
|
|
112
|
+
# Step 7: Check SQLFluff & dbt version requirements
|
|
113
113
|
info_line("")
|
|
114
114
|
step_num = 7 if run_deps else 6
|
|
115
|
-
info_line(f"[{step_num}] Checking SQLFluff version requirements...")
|
|
116
|
-
|
|
117
|
-
if
|
|
118
|
-
info_line(f"
|
|
119
|
-
info_line(" (install with: uv pip install " + " ".join(
|
|
115
|
+
info_line(f"[{step_num}] Checking SQLFluff & dbt version requirements...")
|
|
116
|
+
sqlfluff_requirements = self.download_service.get_sqlfluff_requirements()
|
|
117
|
+
if sqlfluff_requirements:
|
|
118
|
+
info_line(f" SQLFluff: {', '.join(sqlfluff_requirements)}")
|
|
119
|
+
info_line(" (install with: uv pip install " + " ".join(sqlfluff_requirements) + ")")
|
|
120
|
+
dbt_requirements = self.download_service.get_dbt_requirements()
|
|
121
|
+
if dbt_requirements:
|
|
122
|
+
info_line(f" dbt: {', '.join(dbt_requirements)}")
|
|
123
|
+
info_line(" (install with: uv pip install " + " ".join(dbt_requirements) + ")")
|
|
120
124
|
steps_passed += 1
|
|
121
125
|
|
|
122
126
|
# Step 8: Install Cortex Code skills
|
|
@@ -134,7 +138,7 @@ class BootstrapService:
|
|
|
134
138
|
info_line("Your project is ready. Next steps:")
|
|
135
139
|
info_line(f" cd {dbt_project_dir}")
|
|
136
140
|
info_line(" datasecops — run the framework CLI")
|
|
137
|
-
info_line("
|
|
138
|
-
info_line("
|
|
141
|
+
info_line(" dbtf debug — verify Snowflake connection")
|
|
142
|
+
info_line(" dbtf run — run your first models")
|
|
139
143
|
info_line("")
|
|
140
144
|
return True
|
|
@@ -21,7 +21,7 @@ class DbtRunner:
|
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
def _run_command(self, command: str, extra_args: list[str] = None) -> subprocess.CompletedProcess:
|
|
24
|
-
cmd = ["
|
|
24
|
+
cmd = ["dbtf", command] + (extra_args or []) + self._default_args()
|
|
25
25
|
info_line(f"Running: {' '.join(cmd)}")
|
|
26
26
|
result = subprocess.run(cmd, capture_output=False)
|
|
27
27
|
if result.returncode != 0:
|
|
@@ -94,7 +94,7 @@ class DbtRunner:
|
|
|
94
94
|
return self._run_command("docs", ["generate", f"--target={self.target}"])
|
|
95
95
|
|
|
96
96
|
def docs_serve(self) -> subprocess.Popen:
|
|
97
|
-
cmd = ["
|
|
97
|
+
cmd = ["dbtf", "docs", "serve"] + self._default_args()
|
|
98
98
|
info_line(f"Running: {' '.join(cmd)}")
|
|
99
99
|
return subprocess.Popen(cmd)
|
|
100
100
|
|
|
@@ -117,6 +117,9 @@ class DbtRunner:
|
|
|
117
117
|
self._copy_manifest()
|
|
118
118
|
return result
|
|
119
119
|
|
|
120
|
+
def parse(self) -> subprocess.CompletedProcess:
|
|
121
|
+
return self._run_command("parse")
|
|
122
|
+
|
|
120
123
|
def run_operation(self, macro: str, args_str: str = None) -> subprocess.CompletedProcess:
|
|
121
124
|
cmd_args = [f"--target={self.target}"]
|
|
122
125
|
if args_str:
|
{datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/services/download_service.py
RENAMED
|
@@ -159,7 +159,12 @@ class DownloadService:
|
|
|
159
159
|
section_name = self.RULE_SECTION_MAP.get(code)
|
|
160
160
|
if not section_name:
|
|
161
161
|
continue
|
|
162
|
-
opts = entry.get("options", {})
|
|
162
|
+
opts = dict(entry.get("options", {}))
|
|
163
|
+
# For aliasing.length, 0 means "no limit" which sqlfluff expects as None
|
|
164
|
+
if section_name == "aliasing.length":
|
|
165
|
+
for len_key in ("min_alias_length", "max_alias_length"):
|
|
166
|
+
if len_key in opts and opts[len_key] == 0:
|
|
167
|
+
opts[len_key] = None
|
|
163
168
|
self._emit_section(lines, f"sqlfluff:rules:{section_name}", opts)
|
|
164
169
|
|
|
165
170
|
content = "\n".join(lines)
|
|
@@ -168,17 +173,16 @@ class DownloadService:
|
|
|
168
173
|
success_line(f"SQLFluff config written to {dest}")
|
|
169
174
|
return True
|
|
170
175
|
|
|
171
|
-
def
|
|
172
|
-
"""Fetch active
|
|
173
|
-
|
|
176
|
+
def _fetch_framework_versions(self) -> dict[str, str]:
|
|
177
|
+
"""Fetch all active package versions from the framework's DBT_VERSIONS config.
|
|
178
|
+
|
|
179
|
+
Returns a dict mapping pip package name to pinned spec, e.g. {"dbt-core": "dbt-core==1.9.0", ...}.
|
|
180
|
+
"""
|
|
174
181
|
raw = self.sf.get_framework_config("DBT_VERSIONS")
|
|
175
182
|
if not raw:
|
|
176
183
|
error_line("No DBT_VERSIONS configuration found in native app")
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
packages = []
|
|
184
|
+
return {}
|
|
180
185
|
|
|
181
|
-
# Map of config key -> pip package name
|
|
182
186
|
version_keys = {
|
|
183
187
|
"sqlfluff_versions": "sqlfluff",
|
|
184
188
|
"sqlfluff_templater_versions": "sqlfluff-templater-dbt",
|
|
@@ -186,16 +190,35 @@ class DownloadService:
|
|
|
186
190
|
"dbt_snowflake_versions": "dbt-snowflake",
|
|
187
191
|
}
|
|
188
192
|
|
|
193
|
+
result: dict[str, str] = {}
|
|
189
194
|
for config_key, pkg_name in version_keys.items():
|
|
190
195
|
for entry in raw.get(config_key, []):
|
|
191
196
|
if entry.get("active"):
|
|
192
|
-
|
|
197
|
+
result[pkg_name] = f"{pkg_name}=={entry['version']}"
|
|
193
198
|
break
|
|
194
199
|
|
|
195
|
-
if not
|
|
200
|
+
if not result:
|
|
196
201
|
error_line("No active versions found in framework configuration")
|
|
197
202
|
|
|
198
|
-
return
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
def get_sqlfluff_requirements(self) -> list[str]:
|
|
206
|
+
"""Fetch active sqlfluff and sqlfluff-templater-dbt versions from the framework."""
|
|
207
|
+
info_line("Fetching SQLFluff requirements from framework...")
|
|
208
|
+
versions = self._fetch_framework_versions()
|
|
209
|
+
sqlfluff_packages = [versions[k] for k in ("sqlfluff", "sqlfluff-templater-dbt") if k in versions]
|
|
210
|
+
if not sqlfluff_packages:
|
|
211
|
+
error_line("No active SQLFluff versions found in framework configuration")
|
|
212
|
+
return sqlfluff_packages
|
|
213
|
+
|
|
214
|
+
def get_dbt_requirements(self) -> list[str]:
|
|
215
|
+
"""Fetch active dbt-core and dbt-snowflake versions from the framework."""
|
|
216
|
+
info_line("Fetching dbt requirements from framework...")
|
|
217
|
+
versions = self._fetch_framework_versions()
|
|
218
|
+
dbt_packages = [versions[k] for k in ("dbt-core", "dbt-snowflake") if k in versions]
|
|
219
|
+
if not dbt_packages:
|
|
220
|
+
error_line("No active dbt versions found in framework configuration")
|
|
221
|
+
return dbt_packages
|
|
199
222
|
|
|
200
223
|
def download_pipelines(self, platform: str = "github") -> bool:
|
|
201
224
|
info_line(f"Downloading {platform} pipeline configurations...")
|
|
@@ -207,12 +230,12 @@ class DownloadService:
|
|
|
207
230
|
pipelines = raw.get("pipelines", [])
|
|
208
231
|
count = 0
|
|
209
232
|
for pipe in pipelines:
|
|
210
|
-
if pipe.get("platform", "github") != platform or not pipe.get("enabled", True):
|
|
233
|
+
if pipe.get("platform", "github").lower() != platform.lower() or not pipe.get("enabled", True):
|
|
211
234
|
continue
|
|
212
235
|
filename = pipe.get("filename", "")
|
|
213
236
|
yaml_content = pipe.get("yaml_content", "")
|
|
214
237
|
if filename and yaml_content:
|
|
215
|
-
if platform == "github":
|
|
238
|
+
if platform.lower() == "github":
|
|
216
239
|
dest = self.project_dir / ".github" / "workflows" / filename
|
|
217
240
|
else:
|
|
218
241
|
dest = self.project_dir / filename
|
{datasecops_cli-0.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/services/linting_service.py
RENAMED
|
@@ -10,10 +10,16 @@ class LintingService:
|
|
|
10
10
|
def __init__(self, project_dir: Path):
|
|
11
11
|
self.project_dir = project_dir
|
|
12
12
|
|
|
13
|
-
def get_installed_versions(self) -> dict[str, str]:
|
|
14
|
-
"""Get currently installed versions of
|
|
13
|
+
def get_installed_versions(self, packages: list[str] = None) -> dict[str, str]:
|
|
14
|
+
"""Get currently installed versions of the specified packages.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
packages: Package names to check. Defaults to sqlfluff, sqlfluff-templater-dbt, dbt-core, dbt-snowflake.
|
|
18
|
+
"""
|
|
19
|
+
if packages is None:
|
|
20
|
+
packages = ["sqlfluff", "sqlfluff-templater-dbt", "dbt-core", "dbt-snowflake"]
|
|
15
21
|
versions = {}
|
|
16
|
-
for package in
|
|
22
|
+
for package in packages:
|
|
17
23
|
result = subprocess.run(
|
|
18
24
|
["uv", "pip", "show", package],
|
|
19
25
|
capture_output=True, text=True
|
|
@@ -28,7 +34,7 @@ class LintingService:
|
|
|
28
34
|
return versions
|
|
29
35
|
|
|
30
36
|
def install_requirements(self, packages: list[str]) -> bool:
|
|
31
|
-
"""Install
|
|
37
|
+
"""Install packages at specified versions via uv pip."""
|
|
32
38
|
if not packages:
|
|
33
39
|
warning_line("No packages to install")
|
|
34
40
|
return False
|
|
@@ -37,7 +43,7 @@ class LintingService:
|
|
|
37
43
|
info_line(f"Installing: {', '.join(packages)}")
|
|
38
44
|
result = subprocess.run(cmd, capture_output=False)
|
|
39
45
|
if result.returncode == 0:
|
|
40
|
-
success_line("
|
|
46
|
+
success_line("Packages installed successfully")
|
|
41
47
|
return True
|
|
42
48
|
else:
|
|
43
49
|
error_line(f"uv pip install failed with exit code {result.returncode}")
|
|
@@ -75,8 +81,8 @@ class LintingService:
|
|
|
75
81
|
return True
|
|
76
82
|
return self.install_requirements(to_install)
|
|
77
83
|
|
|
78
|
-
# No pinned versions — just ensure packages are present
|
|
79
|
-
missing = [pkg for pkg in ["sqlfluff", "sqlfluff-templater-dbt"
|
|
84
|
+
# No pinned versions — just ensure sqlfluff packages are present
|
|
85
|
+
missing = [pkg for pkg in ["sqlfluff", "sqlfluff-templater-dbt"] if not installed.get(pkg)]
|
|
80
86
|
if not missing:
|
|
81
87
|
return True
|
|
82
88
|
warning_line(f"Missing packages: {', '.join(missing)}")
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Tests for datasecops_cli.main argument parsing and download dispatch."""
|
|
2
|
+
import pytest
|
|
3
|
+
from unittest.mock import MagicMock, patch, call
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from datasecops_cli.main import _build_parser, _run_download, DOWNLOAD_ITEMS
|
|
7
|
+
from datasecops_cli.config import Config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestBuildParser:
|
|
11
|
+
"""Tests for the argparse configuration."""
|
|
12
|
+
|
|
13
|
+
def test_no_args_gives_no_command(self):
|
|
14
|
+
parser = _build_parser()
|
|
15
|
+
args = parser.parse_args([])
|
|
16
|
+
assert args.command is None
|
|
17
|
+
|
|
18
|
+
def test_bootstrap_command(self):
|
|
19
|
+
parser = _build_parser()
|
|
20
|
+
args = parser.parse_args(["bootstrap"])
|
|
21
|
+
assert args.command == "bootstrap"
|
|
22
|
+
|
|
23
|
+
def test_download_single_item(self):
|
|
24
|
+
parser = _build_parser()
|
|
25
|
+
args = parser.parse_args(["download", "sqlfluff"])
|
|
26
|
+
assert args.command == "download"
|
|
27
|
+
assert args.items == ["sqlfluff"]
|
|
28
|
+
|
|
29
|
+
def test_download_multiple_items(self):
|
|
30
|
+
parser = _build_parser()
|
|
31
|
+
args = parser.parse_args(["download", "sqlfluff", "packages", "macros"])
|
|
32
|
+
assert args.command == "download"
|
|
33
|
+
assert args.items == ["sqlfluff", "packages", "macros"]
|
|
34
|
+
|
|
35
|
+
def test_download_all(self):
|
|
36
|
+
parser = _build_parser()
|
|
37
|
+
args = parser.parse_args(["download", "all"])
|
|
38
|
+
assert args.command == "download"
|
|
39
|
+
assert args.items == ["all"]
|
|
40
|
+
|
|
41
|
+
def test_download_invalid_item_exits(self):
|
|
42
|
+
parser = _build_parser()
|
|
43
|
+
with pytest.raises(SystemExit):
|
|
44
|
+
parser.parse_args(["download", "invalid"])
|
|
45
|
+
|
|
46
|
+
def test_download_no_items_exits(self):
|
|
47
|
+
parser = _build_parser()
|
|
48
|
+
with pytest.raises(SystemExit):
|
|
49
|
+
parser.parse_args(["download"])
|
|
50
|
+
|
|
51
|
+
def test_all_download_items_accepted(self):
|
|
52
|
+
parser = _build_parser()
|
|
53
|
+
for item in DOWNLOAD_ITEMS:
|
|
54
|
+
args = parser.parse_args(["download", item])
|
|
55
|
+
assert args.items == [item]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestRunDownload:
|
|
59
|
+
"""Tests for _run_download dispatch logic using mocked services."""
|
|
60
|
+
|
|
61
|
+
def _make_config(self, tmp_path):
|
|
62
|
+
"""Create a Config with minimal valid state."""
|
|
63
|
+
config = Config()
|
|
64
|
+
config.project_dir = tmp_path
|
|
65
|
+
config.dbt_project_dir = tmp_path
|
|
66
|
+
config.profile_name = "test_profile"
|
|
67
|
+
# source_control defaults to GitHub
|
|
68
|
+
return config
|
|
69
|
+
|
|
70
|
+
@patch("datasecops_cli.main._connect_and_load")
|
|
71
|
+
def test_download_sqlfluff(self, mock_connect, tmp_path):
|
|
72
|
+
config = self._make_config(tmp_path)
|
|
73
|
+
mock_sf = MagicMock()
|
|
74
|
+
mock_connect.return_value = mock_sf
|
|
75
|
+
|
|
76
|
+
with patch("datasecops_cli.main.DownloadService") as MockDS:
|
|
77
|
+
mock_ds = MockDS.return_value
|
|
78
|
+
mock_ds.download_sqlfluff_config.return_value = True
|
|
79
|
+
|
|
80
|
+
with pytest.raises(SystemExit) as exc:
|
|
81
|
+
_run_download(config, ["sqlfluff"])
|
|
82
|
+
|
|
83
|
+
assert exc.value.code == 0
|
|
84
|
+
mock_ds.download_sqlfluff_config.assert_called_once()
|
|
85
|
+
mock_ds.download_pipelines.assert_not_called()
|
|
86
|
+
mock_ds.download_dbt_packages.assert_not_called()
|
|
87
|
+
mock_ds.download_macros.assert_not_called()
|
|
88
|
+
|
|
89
|
+
@patch("datasecops_cli.main._connect_and_load")
|
|
90
|
+
def test_download_pipelines(self, mock_connect, tmp_path):
|
|
91
|
+
config = self._make_config(tmp_path)
|
|
92
|
+
mock_sf = MagicMock()
|
|
93
|
+
mock_connect.return_value = mock_sf
|
|
94
|
+
|
|
95
|
+
with patch("datasecops_cli.main.DownloadService") as MockDS:
|
|
96
|
+
mock_ds = MockDS.return_value
|
|
97
|
+
mock_ds.download_pipelines.return_value = True
|
|
98
|
+
|
|
99
|
+
with pytest.raises(SystemExit) as exc:
|
|
100
|
+
_run_download(config, ["pipelines"])
|
|
101
|
+
|
|
102
|
+
assert exc.value.code == 0
|
|
103
|
+
mock_ds.download_pipelines.assert_called_once_with(platform="github")
|
|
104
|
+
|
|
105
|
+
@patch("datasecops_cli.main._connect_and_load")
|
|
106
|
+
def test_download_packages(self, mock_connect, tmp_path):
|
|
107
|
+
config = self._make_config(tmp_path)
|
|
108
|
+
mock_sf = MagicMock()
|
|
109
|
+
mock_connect.return_value = mock_sf
|
|
110
|
+
|
|
111
|
+
with patch("datasecops_cli.main.DownloadService") as MockDS:
|
|
112
|
+
mock_ds = MockDS.return_value
|
|
113
|
+
mock_ds.download_dbt_packages.return_value = True
|
|
114
|
+
|
|
115
|
+
with pytest.raises(SystemExit) as exc:
|
|
116
|
+
_run_download(config, ["packages"])
|
|
117
|
+
|
|
118
|
+
assert exc.value.code == 0
|
|
119
|
+
mock_ds.download_dbt_packages.assert_called_once_with(tmp_path)
|
|
120
|
+
|
|
121
|
+
@patch("datasecops_cli.main._connect_and_load")
|
|
122
|
+
def test_download_macros(self, mock_connect, tmp_path):
|
|
123
|
+
config = self._make_config(tmp_path)
|
|
124
|
+
mock_sf = MagicMock()
|
|
125
|
+
mock_connect.return_value = mock_sf
|
|
126
|
+
|
|
127
|
+
with patch("datasecops_cli.main.DownloadService") as MockDS:
|
|
128
|
+
mock_ds = MockDS.return_value
|
|
129
|
+
mock_ds.download_macros.return_value = True
|
|
130
|
+
|
|
131
|
+
with pytest.raises(SystemExit) as exc:
|
|
132
|
+
_run_download(config, ["macros"])
|
|
133
|
+
|
|
134
|
+
assert exc.value.code == 0
|
|
135
|
+
mock_ds.download_macros.assert_called_once_with("test_profile", tmp_path)
|
|
136
|
+
|
|
137
|
+
@patch("datasecops_cli.main._connect_and_load")
|
|
138
|
+
def test_download_all_expands_to_all_items(self, mock_connect, tmp_path):
|
|
139
|
+
config = self._make_config(tmp_path)
|
|
140
|
+
mock_sf = MagicMock()
|
|
141
|
+
mock_connect.return_value = mock_sf
|
|
142
|
+
|
|
143
|
+
with patch("datasecops_cli.main.DownloadService") as MockDS:
|
|
144
|
+
mock_ds = MockDS.return_value
|
|
145
|
+
mock_ds.download_sqlfluff_config.return_value = True
|
|
146
|
+
mock_ds.download_pipelines.return_value = True
|
|
147
|
+
mock_ds.download_dbt_packages.return_value = True
|
|
148
|
+
mock_ds.download_macros.return_value = True
|
|
149
|
+
|
|
150
|
+
with pytest.raises(SystemExit) as exc:
|
|
151
|
+
_run_download(config, ["all"])
|
|
152
|
+
|
|
153
|
+
assert exc.value.code == 0
|
|
154
|
+
mock_ds.download_sqlfluff_config.assert_called_once()
|
|
155
|
+
mock_ds.download_pipelines.assert_called_once()
|
|
156
|
+
mock_ds.download_dbt_packages.assert_called_once()
|
|
157
|
+
mock_ds.download_macros.assert_called_once()
|
|
158
|
+
|
|
159
|
+
@patch("datasecops_cli.main._connect_and_load")
|
|
160
|
+
def test_download_failure_exits_1(self, mock_connect, tmp_path):
|
|
161
|
+
config = self._make_config(tmp_path)
|
|
162
|
+
mock_sf = MagicMock()
|
|
163
|
+
mock_connect.return_value = mock_sf
|
|
164
|
+
|
|
165
|
+
with patch("datasecops_cli.main.DownloadService") as MockDS:
|
|
166
|
+
mock_ds = MockDS.return_value
|
|
167
|
+
mock_ds.download_sqlfluff_config.return_value = False # failure
|
|
168
|
+
|
|
169
|
+
with pytest.raises(SystemExit) as exc:
|
|
170
|
+
_run_download(config, ["sqlfluff"])
|
|
171
|
+
|
|
172
|
+
assert exc.value.code == 1
|
|
173
|
+
|
|
174
|
+
@patch("datasecops_cli.main._connect_and_load")
|
|
175
|
+
def test_download_partial_failure_exits_1(self, mock_connect, tmp_path):
|
|
176
|
+
"""If one item fails but another succeeds, exit code is still 1."""
|
|
177
|
+
config = self._make_config(tmp_path)
|
|
178
|
+
mock_sf = MagicMock()
|
|
179
|
+
mock_connect.return_value = mock_sf
|
|
180
|
+
|
|
181
|
+
with patch("datasecops_cli.main.DownloadService") as MockDS:
|
|
182
|
+
mock_ds = MockDS.return_value
|
|
183
|
+
mock_ds.download_sqlfluff_config.return_value = True
|
|
184
|
+
mock_ds.download_dbt_packages.return_value = False
|
|
185
|
+
|
|
186
|
+
with pytest.raises(SystemExit) as exc:
|
|
187
|
+
_run_download(config, ["sqlfluff", "packages"])
|
|
188
|
+
|
|
189
|
+
assert exc.value.code == 1
|
|
190
|
+
|
|
191
|
+
@patch("datasecops_cli.main._connect_and_load")
|
|
192
|
+
def test_download_pipelines_uses_configured_platform(self, mock_connect, tmp_path):
|
|
193
|
+
"""Pipeline download reads platform from source_control config."""
|
|
194
|
+
config = self._make_config(tmp_path)
|
|
195
|
+
config.source_control.source_control_platform = "AzureDevOps"
|
|
196
|
+
mock_sf = MagicMock()
|
|
197
|
+
mock_connect.return_value = mock_sf
|
|
198
|
+
|
|
199
|
+
with patch("datasecops_cli.main.DownloadService") as MockDS:
|
|
200
|
+
mock_ds = MockDS.return_value
|
|
201
|
+
mock_ds.download_pipelines.return_value = True
|
|
202
|
+
|
|
203
|
+
with pytest.raises(SystemExit) as exc:
|
|
204
|
+
_run_download(config, ["pipelines"])
|
|
205
|
+
|
|
206
|
+
assert exc.value.code == 0
|
|
207
|
+
mock_ds.download_pipelines.assert_called_once_with(platform="azuredevops")
|
|
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.2.6 → datasecops_cli-0.2.8}/src/datasecops_cli/services/snowflake_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
|