datasecops-cli 0.2.7__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.7 → datasecops_cli-0.2.8}/CHANGELOG.md +15 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/PKG-INFO +54 -8
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/README.md +53 -7
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/docs/getting-started.md +1 -1
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/pyproject.toml +1 -1
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/setup.ps1 +6 -5
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/setup.sh +4 -4
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/main.py +113 -48
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/menus/development.py +1 -1
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/menus/downloads.py +6 -3
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/services/bootstrap_service.py +2 -2
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/services/dbt_runner.py +2 -2
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/services/download_service.py +8 -3
- datasecops_cli-0.2.8/tests/test_main.py +207 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/.github/workflows/publish-cli.yml +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/.gitignore +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/DEVELOPMENT.md +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/LICENSE +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/docs/legacy.md +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/docs/legacy_plan_of_action.md +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/docs/mcp-server.md +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/mcp-servers.json +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/__init__.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/config.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/menus/__init__.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/menus/git_operations.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/models/__init__.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/models/git_helpers.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/models/project_config.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/services/__init__.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/services/git_service.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/services/linting_service.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/services/skill_service.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/services/snowflake_service.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/utilities/__init__.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/utilities/display.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/utilities/file_utils.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_mcp/__init__.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_mcp/__main__.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_mcp/connection.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_mcp/server.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/tests/__init__.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/tests/test_config.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/tests/test_file_utils.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/tests/test_models.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/tests/test_version.py +0 -0
- {datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/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.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
|
+
|
|
5
20
|
## [0.2.7] - 2026-05-12
|
|
6
21
|
|
|
7
22
|
### Changed
|
|
@@ -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?")
|
|
@@ -96,7 +96,7 @@ class DevelopmentMenu:
|
|
|
96
96
|
menu_option(11, "clean - Clean dbt target")
|
|
97
97
|
menu_option(12, "debug - Debug dbt connection")
|
|
98
98
|
menu_option(13, "list - List dbt resources")
|
|
99
|
-
menu_option(14, "install
|
|
99
|
+
menu_option(14, "install lint - Install dbt-core & dbt-snowflake for linting (not dbt Fusion)")
|
|
100
100
|
menu_option(0, "back - Return to main menu")
|
|
101
101
|
|
|
102
102
|
def _run_menu(self) -> None:
|
|
@@ -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.7 → datasecops_cli-0.2.8}/src/datasecops_cli/services/bootstrap_service.py
RENAMED
|
@@ -138,7 +138,7 @@ class BootstrapService:
|
|
|
138
138
|
info_line("Your project is ready. Next steps:")
|
|
139
139
|
info_line(f" cd {dbt_project_dir}")
|
|
140
140
|
info_line(" datasecops — run the framework CLI")
|
|
141
|
-
info_line("
|
|
142
|
-
info_line("
|
|
141
|
+
info_line(" dbtf debug — verify Snowflake connection")
|
|
142
|
+
info_line(" dbtf run — run your first models")
|
|
143
143
|
info_line("")
|
|
144
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
|
|
{datasecops_cli-0.2.7 → 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)
|
|
@@ -225,12 +230,12 @@ class DownloadService:
|
|
|
225
230
|
pipelines = raw.get("pipelines", [])
|
|
226
231
|
count = 0
|
|
227
232
|
for pipe in pipelines:
|
|
228
|
-
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):
|
|
229
234
|
continue
|
|
230
235
|
filename = pipe.get("filename", "")
|
|
231
236
|
yaml_content = pipe.get("yaml_content", "")
|
|
232
237
|
if filename and yaml_content:
|
|
233
|
-
if platform == "github":
|
|
238
|
+
if platform.lower() == "github":
|
|
234
239
|
dest = self.project_dir / ".github" / "workflows" / filename
|
|
235
240
|
else:
|
|
236
241
|
dest = self.project_dir / filename
|
|
@@ -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
|
{datasecops_cli-0.2.7 → datasecops_cli-0.2.8}/src/datasecops_cli/services/linting_service.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.2.7 → 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
|