datasecops-cli 0.7.0b1__tar.gz → 0.7.0b2__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.7.0b1 → datasecops_cli-0.7.0b2}/.gitignore +2 -1
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/CHANGELOG.md +16 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/DEVELOPMENT.md +2 -2
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/PKG-INFO +3 -3
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/README.md +2 -2
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/docs/getting-started.md +38 -1
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/pyproject.toml +1 -1
- datasecops_cli-0.7.0b2/src/datasecops_cli/__init__.py +1 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/main.py +119 -75
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/models/project_config.py +3 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/bootstrap_service.py +3 -1
- datasecops_cli-0.7.0b2/src/datasecops_cli/services/dbt_project_generator.py +331 -0
- datasecops_cli-0.7.0b1/src/datasecops_cli/__init__.py +0 -1
- datasecops_cli-0.7.0b1/src/datasecops_cli/services/dbt_project_generator.py +0 -230
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/.github/workflows/auto-tag.yml +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/.github/workflows/publish-cli.yml +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/.github/workflows/test.yml +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/AGENTS.md +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/LICENSE +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/docs/dcm.md +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/docs/git-operations.md +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/docs/legacy.md +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/docs/legacy_plan_of_action.md +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/docs/mcp-server.md +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/mcp-servers.json +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/scripts/vendor_model.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/setup.ps1 +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/setup.sh +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/config.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/menus/__init__.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/menus/configuration.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/menus/dcm_operations.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/menus/development.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/menus/downloads.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/menus/git_operations.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/models/__init__.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/models/git_helpers.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/__init__.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/dbt_runner.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/dcm_orchestrator.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/dcm_pipelines.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/dcm_registry.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/dcm_service.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/directory_scaffolder.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/download_service.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/git_service.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/linting_service.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/model_commands.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/skill_service.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/snowflake_service.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/upstream_service.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/utilities/__init__.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/utilities/display.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/utilities/file_utils.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/utilities/mcp.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_mcp/__init__.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_mcp/__main__.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_mcp/connection.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_mcp/server.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/__init__.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/convert.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/diagram.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/expand.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/extract.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/render.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/schema.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/scope.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/scripts.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/starter.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/templates/macros/role.sql +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/templates/macros/role_guard.sql +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/templates/macros/warehouse.sql +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/validate.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/__init__.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_config.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_dbt_runner.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_dcm_model.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_dcm_orchestrator.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_dcm_service.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_download_service.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_file_utils.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_linting_service.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_main.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_model_commands.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_models.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_version.py +0 -0
- {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_yaml_utils.py +0 -0
|
@@ -4,6 +4,22 @@ All notable changes to the DataSecOps CLI are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Default app database renamed to `MONITORIAL_ORBIT`** — the `datasecops setup` suggested app-database name (and matching docs examples) was `DATA_ENGINEERS_DATASECOPS_FRAMEWORK`; it now matches the native app's actual installed name.
|
|
10
|
+
- **`dbt_project.yml` generator matches the source-based project spec** — added `docs-paths`, fixed `analysis-paths` (was `analyses`), added top-level `seeds:`/`tests:` blocks, `+static_analysis`/`+meta`/`+tags` at the model root, per-object-type `+static_analysis` (stored procedures/UDFs off, tasks baseline), the commented-out post-hook/on-run-start/on-run-end guidance, and the data-governance `vars` block.
|
|
11
|
+
- **`profiles.yml` deploy targets now run as the framework's CI identity** — every target except `local-dev` previously used the local developer's Snowflake user with each target's own role/warehouse. They now use the CI Username/Role/Warehouse from the native app's Deployment settings (new `ProjectSettings.ci_username`/`ci_role`/`ci_warehouse`, falling back to the previous behavior when unset), matching what the CI pipeline actually deploys with.
|
|
12
|
+
- **`datasecops setup` could silently default to GitHub for a non-GitHub platform** — `PROJECT`/`SOURCE_CONTROL`/`WORK_ITEMS` framework config and profile/`profiles.yml` resolution were one single `try/except`; any failure after connecting (including just the `SOURCE_CONTROL` fetch itself) discarded already-fetched config and silently fell back to `SourceControl()`'s default platform (`GitHub`) with no indication why, so the MCP setup step could prompt for the wrong PAT. Each framework config fetch is now independent, so one failing can't blank out another; a missing/failed `SOURCE_CONTROL` config, or one missing `source_control_platform` specifically, now warns explicitly which platform it's defaulting to.
|
|
13
|
+
- **`local-dev` could be overwritten by a same-named framework target, or by the CI identity on a plain `dbt run`** — `generate_profiles_yml` no longer lets a framework target literally named `local-dev` clobber the dedicated external-browser output, and the profile's default target is now always `local-dev` (previously it followed whatever the native app marked as its own default, which could be a deploy-only target running as the CI identity). `_run_interactive` and bootstrap's `dbt deps` step now always run as `local-dev` instead of trusting the framework's default target.
|
|
14
|
+
- **Re-running `datasecops setup` for an already-known profile never refreshed its deployment targets** — a CI identity change in the native app had no effect until `profiles.yml` was deleted. Setup now always refreshes the CI/deployment targets (while preserving an existing `local-dev` target if already customized), and skips writing `profiles.yml` entirely if the `PROJECT` framework config couldn't be loaded (previously it wrote incorrect defaults).
|
|
15
|
+
- **Hardened `dbt_project.yml` generation against unsafe characters and a YAML boolean-coercion bug** — project/profile name, dbt version and warehouse name are now validated against a safe identifier pattern before being substituted into the raw YAML template (closing a potential injection via a newline or quote in native-app-configured values), and `static_analysis: off` is now quoted so YAML doesn't resolve it to the boolean `false`.
|
|
16
|
+
|
|
17
|
+
### Docs
|
|
18
|
+
|
|
19
|
+
- **Documented setting up `~/.dbt/profiles.yml`** — `datasecops bootstrap` writes a generated `profiles.yml` inside the dbt project folder as a reference only; dbt itself reads from `~/.dbt` (per the native app's `profile_dir` setting), which getting-started.md now explains, including the minimal `local-dev` block a developer needs there.
|
|
20
|
+
|
|
21
|
+
## [0.7.0b1] - 2026-10-26
|
|
22
|
+
|
|
7
23
|
### Added
|
|
8
24
|
|
|
9
25
|
- **Shared DCM model (`datasecops_model`)** — one versioned JSON document (`config/dcm_model.json`, schema `datasecops.dcm/v1`) describing every DCM project (type, dir, `depends_on`, targets with deploy roles), the security model (role groups with environment fan-out, SCIM/UGE prefixes, diagram layout) and the warehouses (the `warehouse()` macro arguments, grants, application grants, budgets). The same package is vendored into the DataSecOps native app (`scripts/vendor_model.py`), so the app and the CLI validate, render and draw identically.
|
|
@@ -38,7 +38,7 @@ chmod +x setup.sh
|
|
|
38
38
|
The script will prompt you for:
|
|
39
39
|
|
|
40
40
|
- **Connection name** — the name of your Snowflake connection from `~/.snowflake/connections.toml`
|
|
41
|
-
- **Native app database name** — the database where the DataSecOps Framework app is installed (e.g. `
|
|
41
|
+
- **Native app database name** — the database where the DataSecOps Framework app is installed (e.g. `MONITORIAL_ORBIT`)
|
|
42
42
|
|
|
43
43
|
### 2. Activate the virtual environment
|
|
44
44
|
|
|
@@ -120,7 +120,7 @@ Created by the setup script in your project root. Contains:
|
|
|
120
120
|
|
|
121
121
|
```yaml
|
|
122
122
|
connection_name: "my_connection" # Snowflake connection name
|
|
123
|
-
app_database: "
|
|
123
|
+
app_database: "MONITORIAL_ORBIT" # Native app database
|
|
124
124
|
profile_name: "" # Auto-detected from dbt_project.yml
|
|
125
125
|
```
|
|
126
126
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: datasecops-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.0b2
|
|
4
4
|
Summary: DataSecOps Framework CLI for Snowflake Native App
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -120,7 +120,7 @@ Your pipeline needs two things:
|
|
|
120
120
|
|
|
121
121
|
```yaml
|
|
122
122
|
connection_name: "ci"
|
|
123
|
-
app_database: "
|
|
123
|
+
app_database: "MONITORIAL_ORBIT"
|
|
124
124
|
```
|
|
125
125
|
|
|
126
126
|
2. **A Snowflake connection** in `~/.snowflake/connections.toml` for the CI service account:
|
|
@@ -264,7 +264,7 @@ The CLI reads from a `.datasecops.yml` file in your project root (created by the
|
|
|
264
264
|
|
|
265
265
|
```yaml
|
|
266
266
|
connection_name: "my_connection"
|
|
267
|
-
app_database: "
|
|
267
|
+
app_database: "MONITORIAL_ORBIT"
|
|
268
268
|
```
|
|
269
269
|
|
|
270
270
|
Project profiles, linting rules, pipeline templates, and deployment targets are all managed centrally in the native app and pulled down by the CLI.
|
|
@@ -100,7 +100,7 @@ Your pipeline needs two things:
|
|
|
100
100
|
|
|
101
101
|
```yaml
|
|
102
102
|
connection_name: "ci"
|
|
103
|
-
app_database: "
|
|
103
|
+
app_database: "MONITORIAL_ORBIT"
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
2. **A Snowflake connection** in `~/.snowflake/connections.toml` for the CI service account:
|
|
@@ -244,7 +244,7 @@ The CLI reads from a `.datasecops.yml` file in your project root (created by the
|
|
|
244
244
|
|
|
245
245
|
```yaml
|
|
246
246
|
connection_name: "my_connection"
|
|
247
|
-
app_database: "
|
|
247
|
+
app_database: "MONITORIAL_ORBIT"
|
|
248
248
|
```
|
|
249
249
|
|
|
250
250
|
Project profiles, linting rules, pipeline templates, and deployment targets are all managed centrally in the native app and pulled down by the CLI.
|
|
@@ -59,7 +59,7 @@ This creates `.datasecops.yml` in your project root:
|
|
|
59
59
|
|
|
60
60
|
```yaml
|
|
61
61
|
connection_name: "my_connection"
|
|
62
|
-
app_database: "
|
|
62
|
+
app_database: "MONITORIAL_ORBIT"
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
If you prefer to create it manually, the two required fields are:
|
|
@@ -91,6 +91,43 @@ You should see the main menu, confirming the CLI can reach the native app.
|
|
|
91
91
|
|
|
92
92
|
---
|
|
93
93
|
|
|
94
|
+
## Step 3b: Set up your local dbt profile (`~/.dbt/profiles.yml`)
|
|
95
|
+
|
|
96
|
+
Running `datasecops bootstrap` (or **Configuration → new project** from the main menu) scaffolds a
|
|
97
|
+
dbt project from the native app's framework config, including a generated `dbt_project.yml` and
|
|
98
|
+
`profiles.yml`. The generated `profiles.yml` is written *inside* the dbt project folder (e.g.
|
|
99
|
+
`./dbt/profiles.yml`) as a reference — it's gitignored and never committed.
|
|
100
|
+
|
|
101
|
+
dbt itself does **not** read that file. It reads whatever `profiles.yml` lives in the directory set
|
|
102
|
+
by `--profiles-dir`, which the CLI passes as the native app's `PROJECT.profile_dir` setting
|
|
103
|
+
(defaults to `~/.dbt`). So you need to copy the generated `local-dev` target into your own
|
|
104
|
+
`~/.dbt/profiles.yml` (create the file/folder if they don't exist yet):
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
<profile_name>: # matches the `profile:` field in dbt_project.yml
|
|
108
|
+
target: local-dev
|
|
109
|
+
outputs:
|
|
110
|
+
local-dev:
|
|
111
|
+
type: snowflake
|
|
112
|
+
account: "<your_account>"
|
|
113
|
+
authenticator: externalbrowser # SSO login, no password/PAT needed
|
|
114
|
+
role: DEVELOPERS
|
|
115
|
+
warehouse: DEV_WH
|
|
116
|
+
database: "<profile's target database>"
|
|
117
|
+
schema: PUBLIC
|
|
118
|
+
threads: 4
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
You only need the `local-dev` target locally. The other targets in the generated `profiles.yml`
|
|
122
|
+
(e.g. `dev`, `test`, `prod`) run as the framework's CI/deployment identity (the CI Username, Role
|
|
123
|
+
and Warehouse configured in the native app's Deployment settings) — they're what the CI pipeline
|
|
124
|
+
deploys with, not something a developer runs against directly.
|
|
125
|
+
|
|
126
|
+
If you work on multiple framework projects, each profile (one per project, named after its
|
|
127
|
+
`profile:` field) gets its own block in the same `~/.dbt/profiles.yml` file.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
94
131
|
## Step 4: Install MCP servers
|
|
95
132
|
|
|
96
133
|
### 4a. DataSecOps Framework MCP Server (included)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.0b2"
|
|
@@ -475,7 +475,7 @@ def _run_setup(project_dir: Path):
|
|
|
475
475
|
# --- App database ---
|
|
476
476
|
app_database = get_input_string_or_default(
|
|
477
477
|
"Enter native app database name",
|
|
478
|
-
"
|
|
478
|
+
"MONITORIAL_ORBIT"
|
|
479
479
|
)
|
|
480
480
|
|
|
481
481
|
# --- CoCo agent connection ---
|
|
@@ -496,104 +496,146 @@ def _run_setup(project_dir: Path):
|
|
|
496
496
|
source_control = SourceControl()
|
|
497
497
|
work_items = WorkItems()
|
|
498
498
|
matched_profile = None
|
|
499
|
+
account_identifier = ""
|
|
500
|
+
project_settings_loaded = False
|
|
499
501
|
|
|
502
|
+
connected = False
|
|
500
503
|
try:
|
|
501
504
|
temp_sf.connect()
|
|
502
505
|
success_line("Connected")
|
|
506
|
+
connected = True
|
|
507
|
+
except Exception as e:
|
|
508
|
+
warning_line(f"Could not connect to Snowflake: {e}")
|
|
509
|
+
warning_line("You can set profile_name manually in .datasecops.yml")
|
|
510
|
+
warning_line("You may need to create profiles.yml manually")
|
|
503
511
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
512
|
+
profile_name = ""
|
|
513
|
+
if connected:
|
|
514
|
+
# Each framework config code is fetched independently: a failure (or a missing
|
|
515
|
+
# config row) must not discard config that was already fetched successfully, and
|
|
516
|
+
# must not silently fall back to a default (e.g. GitHub) without saying so.
|
|
517
|
+
try:
|
|
518
|
+
raw_project = temp_sf.get_framework_config("PROJECT")
|
|
519
|
+
if raw_project:
|
|
520
|
+
project_settings = ProjectSettings(**{k: v for k, v in raw_project.items() if k in ProjectSettings.model_fields})
|
|
521
|
+
project_settings_loaded = True
|
|
522
|
+
else:
|
|
523
|
+
warning_line("No PROJECT framework config found in native app - using defaults")
|
|
524
|
+
except Exception as e:
|
|
525
|
+
warning_line(f"Could not load PROJECT framework config: {e}")
|
|
508
526
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
527
|
+
try:
|
|
528
|
+
raw_sc = temp_sf.get_framework_config("SOURCE_CONTROL")
|
|
529
|
+
if raw_sc:
|
|
530
|
+
if "source_control_platform" not in raw_sc:
|
|
531
|
+
warning_line(f"SOURCE_CONTROL framework config has no source_control_platform - "
|
|
532
|
+
f"defaulting to {source_control.source_control_platform}")
|
|
533
|
+
source_control = SourceControl(**{k: v for k, v in raw_sc.items() if k in SourceControl.model_fields})
|
|
534
|
+
else:
|
|
535
|
+
warning_line(f"No SOURCE_CONTROL framework config found in native app - "
|
|
536
|
+
f"defaulting source control platform to {source_control.source_control_platform}")
|
|
537
|
+
except Exception as e:
|
|
538
|
+
warning_line(f"Could not load SOURCE_CONTROL framework config "
|
|
539
|
+
f"(defaulting source control platform to {source_control.source_control_platform}): {e}")
|
|
513
540
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
541
|
+
try:
|
|
542
|
+
raw_wi = temp_sf.get_framework_config("WORK_ITEMS")
|
|
543
|
+
if raw_wi:
|
|
544
|
+
work_items = WorkItems(**{k: v for k, v in raw_wi.items() if k in WorkItems.model_fields})
|
|
545
|
+
except Exception as e:
|
|
546
|
+
warning_line(f"Could not load WORK_ITEMS framework config: {e}")
|
|
518
547
|
|
|
519
548
|
# Get account identifier for MCP URL construction
|
|
520
|
-
account_identifier = ""
|
|
521
549
|
try:
|
|
522
550
|
org_rows = temp_sf.execute_query("SELECT CURRENT_ORGANIZATION_NAME() || '-' || CURRENT_ACCOUNT_NAME() AS acct")
|
|
523
551
|
account_identifier = org_rows[0]["ACCT"].lower() if org_rows else ""
|
|
524
552
|
except Exception as e:
|
|
525
553
|
warning_line(f"Could not determine account identifier (AI Agent MCP will be skipped): {e}")
|
|
526
554
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
555
|
+
try:
|
|
556
|
+
# --- Resolve profile name ---
|
|
557
|
+
from datasecops_cli.utilities.yaml_utils import get_profile_name
|
|
558
|
+
profile_name = get_profile_name(project_dir)
|
|
559
|
+
if profile_name:
|
|
560
|
+
success_line(f"Profile found in dbt_project.yml: {profile_name}")
|
|
561
|
+
else:
|
|
562
|
+
info_line("No dbt_project.yml found. Fetching project profiles...")
|
|
563
|
+
profiles_data = temp_sf.get_project_profiles()
|
|
564
|
+
if profiles_data:
|
|
565
|
+
profile_names = [p.get("PROFILE_NAME", p.get("profile_name", "")) for p in profiles_data]
|
|
566
|
+
profile_names = [n for n in profile_names if n]
|
|
567
|
+
if len(profile_names) == 1:
|
|
568
|
+
profile_name = profile_names[0]
|
|
569
|
+
success_line(f"Auto-selected profile: {profile_name}")
|
|
570
|
+
elif len(profile_names) > 1:
|
|
571
|
+
info_line(f"Found {len(profile_names)} project profiles:")
|
|
572
|
+
profile_name = select_from_list(profile_names, "profile", add_back=False)
|
|
573
|
+
else:
|
|
574
|
+
warning_line("No profiles found in native app")
|
|
544
575
|
else:
|
|
545
576
|
warning_line("No profiles found in native app")
|
|
546
|
-
else:
|
|
547
|
-
warning_line("No profiles found in native app")
|
|
548
|
-
|
|
549
|
-
# Find the full profile object for profiles.yml generation
|
|
550
|
-
if profile_name:
|
|
551
|
-
profiles_data = temp_sf.get_project_profiles()
|
|
552
|
-
if profiles_data:
|
|
553
|
-
for p in profiles_data:
|
|
554
|
-
pn = p.get("PROFILE_NAME", p.get("profile_name", ""))
|
|
555
|
-
if pn == profile_name:
|
|
556
|
-
matched_profile = ProjectProfile(**{k.lower(): v for k, v in p.items()})
|
|
557
|
-
break
|
|
558
|
-
|
|
559
|
-
# --- Ensure profiles.yml has the profile entry ---
|
|
560
|
-
if profile_name and matched_profile:
|
|
561
|
-
import yaml
|
|
562
|
-
from datasecops_cli.utilities.file_utils import ensure_dir, write_file
|
|
563
|
-
from datasecops_cli.services.dbt_project_generator import generate_profiles_yml
|
|
564
577
|
|
|
565
|
-
|
|
566
|
-
|
|
578
|
+
# Find the full profile object for profiles.yml generation
|
|
579
|
+
if profile_name:
|
|
580
|
+
profiles_data = temp_sf.get_project_profiles()
|
|
581
|
+
if profiles_data:
|
|
582
|
+
for p in profiles_data:
|
|
583
|
+
pn = p.get("PROFILE_NAME", p.get("profile_name", ""))
|
|
584
|
+
if pn == profile_name:
|
|
585
|
+
matched_profile = ProjectProfile(**{k.lower(): v for k, v in p.items()})
|
|
586
|
+
break
|
|
587
|
+
|
|
588
|
+
# --- Ensure profiles.yml has the profile entry ---
|
|
589
|
+
if profile_name and matched_profile and not project_settings_loaded:
|
|
590
|
+
warning_line(f"Skipping profiles.yml for '{profile_name}': PROJECT framework config "
|
|
591
|
+
"was not loaded, so the correct CI identity/targets aren't known")
|
|
592
|
+
elif profile_name and matched_profile:
|
|
593
|
+
import yaml
|
|
594
|
+
from datasecops_cli.utilities.file_utils import ensure_dir, write_file
|
|
595
|
+
from datasecops_cli.services.dbt_project_generator import generate_profiles_yml
|
|
596
|
+
|
|
597
|
+
profiles_dir = Path(project_settings.profile_dir).expanduser()
|
|
598
|
+
profiles_path = profiles_dir / "profiles.yml"
|
|
599
|
+
|
|
600
|
+
# Read existing profiles.yml or start fresh
|
|
601
|
+
existing_profiles = {}
|
|
602
|
+
if profiles_path.exists():
|
|
603
|
+
with open(profiles_path, "r", encoding="utf-8") as f:
|
|
604
|
+
existing_profiles = yaml.safe_load(f) or {}
|
|
567
605
|
|
|
568
|
-
# Read existing profiles.yml or start fresh
|
|
569
|
-
existing_profiles = {}
|
|
570
|
-
if profiles_path.exists():
|
|
571
|
-
with open(profiles_path, "r", encoding="utf-8") as f:
|
|
572
|
-
existing_profiles = yaml.safe_load(f) or {}
|
|
573
|
-
|
|
574
|
-
if profile_name in existing_profiles:
|
|
575
|
-
success_line(f"Profile '{profile_name}' already exists in {profiles_path}")
|
|
576
|
-
else:
|
|
577
|
-
info_line(f"Adding profile '{profile_name}' to {profiles_path}...")
|
|
578
606
|
account_info = temp_sf.get_account_info()
|
|
579
607
|
account = account_info.get("ACCOUNT", account_info.get("account", ""))
|
|
580
608
|
user = account_info.get("USER_NAME", account_info.get("user_name", ""))
|
|
581
609
|
new_profiles_yaml = generate_profiles_yml(matched_profile, project_settings, account, user)
|
|
582
610
|
new_profiles = yaml.safe_load(new_profiles_yaml) or {}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
611
|
+
new_outputs = new_profiles.get(profile_name, {}).get("outputs", {})
|
|
612
|
+
|
|
613
|
+
if profile_name in existing_profiles:
|
|
614
|
+
# Refresh the CI/deployment targets every run (so a CI identity change in
|
|
615
|
+
# the native app takes effect), but keep an existing local-dev target as
|
|
616
|
+
# the developer may have customized it.
|
|
617
|
+
existing_outputs = existing_profiles[profile_name].setdefault("outputs", {})
|
|
618
|
+
for name, output in new_outputs.items():
|
|
619
|
+
if name == "local-dev" and name in existing_outputs:
|
|
620
|
+
continue
|
|
621
|
+
existing_outputs[name] = output
|
|
622
|
+
existing_profiles[profile_name].setdefault("target", new_profiles[profile_name]["target"])
|
|
623
|
+
write_file(profiles_path, yaml.dump(existing_profiles, default_flow_style=False, sort_keys=False))
|
|
624
|
+
success_line(f"Updated deployment targets for profile '{profile_name}' in {profiles_path}")
|
|
625
|
+
else:
|
|
626
|
+
info_line(f"Adding profile '{profile_name}' to {profiles_path}...")
|
|
627
|
+
existing_profiles.update(new_profiles)
|
|
628
|
+
ensure_dir(profiles_dir)
|
|
629
|
+
write_file(profiles_path, yaml.dump(existing_profiles, default_flow_style=False, sort_keys=False))
|
|
630
|
+
success_line(f"Profile '{profile_name}' written to {profiles_path}")
|
|
631
|
+
except Exception as e:
|
|
632
|
+
warning_line(f"Could not resolve the project profile / write profiles.yml: {e}")
|
|
633
|
+
warning_line("You can set profile_name manually in .datasecops.yml")
|
|
634
|
+
warning_line("You may need to create profiles.yml manually")
|
|
635
|
+
from datasecops_cli.utilities.yaml_utils import get_profile_name
|
|
636
|
+
profile_name = get_profile_name(project_dir) or ""
|
|
587
637
|
|
|
588
|
-
|
|
589
|
-
warning_line(f"Could not connect to Snowflake: {e}")
|
|
590
|
-
warning_line("You can set profile_name manually in .datasecops.yml")
|
|
591
|
-
warning_line("You may need to create profiles.yml manually")
|
|
592
|
-
# Fall back to reading profile from dbt_project.yml if available
|
|
593
|
-
from datasecops_cli.utilities.yaml_utils import get_profile_name
|
|
594
|
-
profile_name = get_profile_name(project_dir) or ""
|
|
595
|
-
finally:
|
|
596
|
-
temp_sf.close()
|
|
638
|
+
temp_sf.close()
|
|
597
639
|
|
|
598
640
|
# --- Write config ---
|
|
599
641
|
config_data = {
|
|
@@ -860,7 +902,9 @@ def _run_interactive(config: Config):
|
|
|
860
902
|
dbt_runner = DbtRunner(
|
|
861
903
|
project_dir=config.dbt_project_dir,
|
|
862
904
|
profiles_dir=config.get_dbt_profiles_dir(),
|
|
863
|
-
|
|
905
|
+
# Always the developer's own identity, never the framework's CI/deployment
|
|
906
|
+
# default target (which may be a deploy-only target running as the CI identity).
|
|
907
|
+
target="local-dev",
|
|
864
908
|
engine=config.datasecops.dbt_engine or "dbt_core"
|
|
865
909
|
)
|
|
866
910
|
|
{datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/models/project_config.py
RENAMED
|
@@ -40,6 +40,9 @@ class ProjectSettings(BaseModel):
|
|
|
40
40
|
execution_mode: str = "dbt_cli"
|
|
41
41
|
dbt_engine: str = "dbt_core"
|
|
42
42
|
ai_agents_enabled: bool = False
|
|
43
|
+
ci_username: str = ""
|
|
44
|
+
ci_role: str = ""
|
|
45
|
+
ci_warehouse: str = ""
|
|
43
46
|
targets: list[DbtTarget] = Field(default_factory=lambda: [
|
|
44
47
|
DbtTarget(target_name="dev", branch_name="dev", target_role="DEVELOPERS", target_warehouse="DEV_WH", is_default=True),
|
|
45
48
|
DbtTarget(target_name="test", branch_name="test", target_role="DATAOPS_ADMIN", target_warehouse="DATAOPS_WH"),
|
{datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/bootstrap_service.py
RENAMED
|
@@ -112,7 +112,9 @@ class BootstrapService:
|
|
|
112
112
|
dbt_runner = DbtRunner(
|
|
113
113
|
project_dir=dbt_project_dir,
|
|
114
114
|
profiles_dir=Path(self.project_settings.profile_dir).expanduser(),
|
|
115
|
-
|
|
115
|
+
# Always the developer's own identity, never the framework's CI/deployment
|
|
116
|
+
# default target.
|
|
117
|
+
target="local-dev",
|
|
116
118
|
engine=self.datasecops_config.dbt_engine,
|
|
117
119
|
)
|
|
118
120
|
result = dbt_runner.deps()
|