datasecops-cli 0.7.0b2__tar.gz → 0.7.0b5__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.0b2 → datasecops_cli-0.7.0b5}/CHANGELOG.md +15 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/PKG-INFO +28 -1
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/README.md +27 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/docs/getting-started.md +23 -17
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/pyproject.toml +1 -1
- datasecops_cli-0.7.0b5/src/datasecops_cli/__init__.py +1 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/config.py +6 -2
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/main.py +81 -49
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/models/project_config.py +1 -3
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/dbt_project_generator.py +89 -56
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/download_service.py +12 -8
- datasecops_cli-0.7.0b5/src/datasecops_cli/services/pipeline_env.py +93 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/snowflake_service.py +30 -1
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_mcp/connection.py +4 -1
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/test_config.py +7 -0
- datasecops_cli-0.7.0b5/tests/test_dbt_project_generator_profiles.py +150 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/test_main.py +40 -1
- datasecops_cli-0.7.0b5/tests/test_pipeline_env.py +108 -0
- datasecops_cli-0.7.0b2/src/datasecops_cli/__init__.py +0 -1
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/.github/workflows/auto-tag.yml +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/.github/workflows/publish-cli.yml +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/.github/workflows/test.yml +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/.gitignore +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/AGENTS.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/DEVELOPMENT.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/LICENSE +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/docs/dcm.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/docs/git-operations.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/docs/legacy.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/docs/legacy_plan_of_action.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/docs/mcp-server.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/mcp-servers.json +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/scripts/vendor_model.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/setup.ps1 +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/setup.sh +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/menus/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/menus/configuration.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/menus/dcm_operations.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/menus/development.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/menus/downloads.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/menus/git_operations.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/models/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/models/git_helpers.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/bootstrap_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/dbt_runner.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/dcm_orchestrator.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/dcm_pipelines.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/dcm_registry.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/dcm_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/directory_scaffolder.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/git_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/linting_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/model_commands.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/skill_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/upstream_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/utilities/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/utilities/display.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/utilities/file_utils.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/utilities/mcp.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_mcp/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_mcp/__main__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_mcp/server.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/convert.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/diagram.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/expand.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/extract.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/render.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/schema.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/scope.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/scripts.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/starter.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/templates/macros/role.sql +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/templates/macros/role_guard.sql +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/templates/macros/warehouse.sql +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_model/validate.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/test_dbt_runner.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/test_dcm_model.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/test_dcm_orchestrator.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/test_dcm_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/test_download_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/test_file_utils.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/test_linting_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/test_model_commands.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/test_models.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/test_version.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/tests/test_yaml_utils.py +0 -0
|
@@ -4,6 +4,21 @@ All notable changes to the DataSecOps CLI are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.7.0b5] - 2026-10-26
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`datasecops pipeline env`** — resolves the dbt target for a pipeline run from the native app's dbt Targets (`--branch` matches a target's Branch, then a target named like the branch; `--target` picks one by name) and sets `TARGET`, `SF_ROLE`, `SF_WAREHOUSE` and `TARGET_SCHEMA` (`$GITHUB_ENV` on GitHub Actions, `##vso` on Azure DevOps, `KEY=value` otherwise). Generated pipelines use it instead of hard-coding the CI role and warehouse.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **`profiles.yml` deployment targets were being written to a developer's own `~/.dbt/profiles.yml`** — CI/deployment targets (`dev`/`test`/`prod`) belong to the native app's CI pipeline, not a developer's machine, so they're no longer written there at all. Only the `local-dev` target is ever added or refreshed; any other targets already present in the file (added manually, or by a framework target sharing the profile's name) are left untouched. Removed the now-unused CI-identity profile generation (`ProjectSettings.ci_username`, the old `generate_profiles_yml`).
|
|
16
|
+
- **`local-dev` used a hardcoded `authenticator: externalbrowser` and a shared database instead of the developer's own identity** — `local-dev` now reuses whatever Snowflake CLI connection is already configured for the project in `~/.snowflake/connections.toml` (account, user, and whatever auth method is already in use — key pair, SSO, password, etc.), read directly via `snowflake.connector.config_manager` without establishing a connection. `role`/`warehouse`/`schema` still always come from the project's dbt targets config. The developer is prompted once for their name (persisted to `.datasecops.yml`), used to build a personal sandpit database (`<target_database>SANDPIT_<name>`) so multiple developers never collide.
|
|
17
|
+
- **`profiles.yml` is no longer written inside the dbt project folder** — dbt never reads a `profiles.yml` there (it reads `--profiles-dir`, i.e. `~/.dbt` per the native app's `profile_dir` setting), so the project-local copy was always a dead reference file. `datasecops setup`/`bootstrap`/`new project` now write/merge the `local-dev` target directly into `~/.dbt/profiles.yml` via a shared `write_profiles_yml()` helper, creating the folder/file if needed.
|
|
18
|
+
- **`app_database` is validated before it is used in SQL** — it is interpolated into the native app `CALL` statements (CLI and MCP server), so a value that isn't a single plain or double-quoted identifier is now rejected instead of being able to change the statement.
|
|
19
|
+
|
|
20
|
+
## [0.7.0b2] - 2026-10-26
|
|
21
|
+
|
|
7
22
|
### Fixed
|
|
8
23
|
|
|
9
24
|
- **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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: datasecops-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.0b5
|
|
4
4
|
Summary: DataSecOps Framework CLI for Snowflake Native App
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -146,6 +146,33 @@ Your pipeline needs two things:
|
|
|
146
146
|
|
|
147
147
|
The exit code is `0` on success, `1` if any download fails.
|
|
148
148
|
|
|
149
|
+
### Environment settings for the branch
|
|
150
|
+
|
|
151
|
+
`datasecops pipeline env` reads the dbt Targets from the native app (**Settings › Data › dbt
|
|
152
|
+
Targets**) and sets `TARGET`, `SF_ROLE`, `SF_WAREHOUSE` and `TARGET_SCHEMA` for the target mapped to
|
|
153
|
+
the branch, so pipelines don't hard-code them:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
datasecops pipeline env --branch "$GITHUB_REF_NAME" # GitHub Actions: target whose Branch is this branch
|
|
157
|
+
datasecops pipeline env --branch "$BUILD_SOURCEBRANCH" # Azure DevOps (the full ref, not BUILD_SOURCEBRANCHNAME)
|
|
158
|
+
datasecops pipeline env --target unit-test # a named target
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Pass the branch through the runner's environment variable, not a `${{ ... }}` / `$(...)` expression:
|
|
162
|
+
those are substituted into the script before the shell runs, so a branch name containing `$(...)`
|
|
163
|
+
would execute.
|
|
164
|
+
|
|
165
|
+
- `--branch` matches a target's **Branch**, then a target named like the branch; `refs/heads/` is
|
|
166
|
+
ignored and a pull request's merge ref resolves to its source branch (`$GITHUB_HEAD_REF` on GitHub,
|
|
167
|
+
`$SYSTEM_PULLREQUEST_SOURCEBRANCH` on Azure DevOps). On Azure DevOps pass `$BUILD_SOURCEBRANCH`:
|
|
168
|
+
`$BUILD_SOURCEBRANCHNAME` is only the last part of the ref (`release/prod` -> `prod`).
|
|
169
|
+
Exact names win; a case-insensitive match is used only when it is unambiguous.
|
|
170
|
+
- GitHub Actions: the values are appended to `$GITHUB_ENV`. Azure DevOps: they are set with
|
|
171
|
+
`##vso[task.setvariable]`. Elsewhere (or `--format shell`) they are printed as shell-quoted `KEY=value` lines.
|
|
172
|
+
- A target with no role, warehouse or schema leaves that variable as it is.
|
|
173
|
+
- The command connects with `.datasecops.yml`'s connection, so the CI user needs the `orbit_developer`
|
|
174
|
+
application role. It exits `1` when no target matches.
|
|
175
|
+
|
|
149
176
|
## DCM projects
|
|
150
177
|
|
|
151
178
|
```bash
|
|
@@ -126,6 +126,33 @@ Your pipeline needs two things:
|
|
|
126
126
|
|
|
127
127
|
The exit code is `0` on success, `1` if any download fails.
|
|
128
128
|
|
|
129
|
+
### Environment settings for the branch
|
|
130
|
+
|
|
131
|
+
`datasecops pipeline env` reads the dbt Targets from the native app (**Settings › Data › dbt
|
|
132
|
+
Targets**) and sets `TARGET`, `SF_ROLE`, `SF_WAREHOUSE` and `TARGET_SCHEMA` for the target mapped to
|
|
133
|
+
the branch, so pipelines don't hard-code them:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
datasecops pipeline env --branch "$GITHUB_REF_NAME" # GitHub Actions: target whose Branch is this branch
|
|
137
|
+
datasecops pipeline env --branch "$BUILD_SOURCEBRANCH" # Azure DevOps (the full ref, not BUILD_SOURCEBRANCHNAME)
|
|
138
|
+
datasecops pipeline env --target unit-test # a named target
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Pass the branch through the runner's environment variable, not a `${{ ... }}` / `$(...)` expression:
|
|
142
|
+
those are substituted into the script before the shell runs, so a branch name containing `$(...)`
|
|
143
|
+
would execute.
|
|
144
|
+
|
|
145
|
+
- `--branch` matches a target's **Branch**, then a target named like the branch; `refs/heads/` is
|
|
146
|
+
ignored and a pull request's merge ref resolves to its source branch (`$GITHUB_HEAD_REF` on GitHub,
|
|
147
|
+
`$SYSTEM_PULLREQUEST_SOURCEBRANCH` on Azure DevOps). On Azure DevOps pass `$BUILD_SOURCEBRANCH`:
|
|
148
|
+
`$BUILD_SOURCEBRANCHNAME` is only the last part of the ref (`release/prod` -> `prod`).
|
|
149
|
+
Exact names win; a case-insensitive match is used only when it is unambiguous.
|
|
150
|
+
- GitHub Actions: the values are appended to `$GITHUB_ENV`. Azure DevOps: they are set with
|
|
151
|
+
`##vso[task.setvariable]`. Elsewhere (or `--format shell`) they are printed as shell-quoted `KEY=value` lines.
|
|
152
|
+
- A target with no role, warehouse or schema leaves that variable as it is.
|
|
153
|
+
- The command connects with `.datasecops.yml`'s connection, so the CI user needs the `orbit_developer`
|
|
154
|
+
application role. It exits `1` when no target matches.
|
|
155
|
+
|
|
129
156
|
## DCM projects
|
|
130
157
|
|
|
131
158
|
```bash
|
|
@@ -93,15 +93,24 @@ You should see the main menu, confirming the CLI can reach the native app.
|
|
|
93
93
|
|
|
94
94
|
## Step 3b: Set up your local dbt profile (`~/.dbt/profiles.yml`)
|
|
95
95
|
|
|
96
|
-
Running `datasecops bootstrap` (or **Configuration → new project** from the
|
|
97
|
-
|
|
98
|
-
`
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
(
|
|
104
|
-
|
|
96
|
+
Running `datasecops setup` or `datasecops bootstrap` (or **Configuration → new project** from the
|
|
97
|
+
main menu) writes a `local-dev` target for your profile directly to `~/.dbt/profiles.yml` (per the
|
|
98
|
+
native app's `PROJECT.profile_dir` setting, used as dbt's `--profiles-dir`) — not inside the dbt
|
|
99
|
+
project folder, since dbt never reads a `profiles.yml` there. The `.dbt` folder and `profiles.yml`
|
|
100
|
+
file are created if they don't exist yet; if a profile entry already exists, only its `local-dev`
|
|
101
|
+
target is refreshed — any other targets already in the file (e.g. CI/deployment targets you or a
|
|
102
|
+
pipeline added) are left untouched. **Only `local-dev` is ever written here** — deployment targets
|
|
103
|
+
(`dev`/`test`/`prod`) belong to the native app's CI pipeline, never to a developer's machine.
|
|
104
|
+
|
|
105
|
+
The `local-dev` target reuses whatever Snowflake CLI connection you already have configured in
|
|
106
|
+
`~/.snowflake/connections.toml` for the `connection_name` in your `.datasecops.yml` — account, user,
|
|
107
|
+
and whatever auth method you already use (external browser, key pair, password, etc.) are copied
|
|
108
|
+
across as-is, so there's nothing extra to configure. You'll be asked for your name once (used to
|
|
109
|
+
build your personal sandpit database, e.g. `jon` → `SDS_SANDPIT_JON`), and it's saved to
|
|
110
|
+
`.datasecops.yml` so you won't be asked again. `role`/`warehouse`/`schema` always come from the
|
|
111
|
+
project's dbt targets config, never from connections.toml.
|
|
112
|
+
|
|
113
|
+
The resulting `~/.dbt/profiles.yml` looks like:
|
|
105
114
|
|
|
106
115
|
```yaml
|
|
107
116
|
<profile_name>: # matches the `profile:` field in dbt_project.yml
|
|
@@ -109,20 +118,17 @@ by `--profiles-dir`, which the CLI passes as the native app's `PROJECT.profile_d
|
|
|
109
118
|
outputs:
|
|
110
119
|
local-dev:
|
|
111
120
|
type: snowflake
|
|
112
|
-
account: "<
|
|
113
|
-
|
|
121
|
+
account: "<from your connections.toml connection>"
|
|
122
|
+
user: "<from your connections.toml connection>"
|
|
123
|
+
authenticator: "<from your connections.toml connection, if set>"
|
|
124
|
+
private_key_path: "<from your connections.toml connection, if set>"
|
|
114
125
|
role: DEVELOPERS
|
|
115
126
|
warehouse: DEV_WH
|
|
116
|
-
database: "<
|
|
127
|
+
database: "<target_database>SANDPIT_<your name>"
|
|
117
128
|
schema: PUBLIC
|
|
118
129
|
threads: 4
|
|
119
130
|
```
|
|
120
131
|
|
|
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
132
|
If you work on multiple framework projects, each profile (one per project, named after its
|
|
127
133
|
`profile:` field) gets its own block in the same `~/.dbt/profiles.yml` file.
|
|
128
134
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.0b5"
|
|
@@ -23,8 +23,9 @@ class Config:
|
|
|
23
23
|
self.dbt_project_dir: Path = Path.cwd()
|
|
24
24
|
self.profile_name: str = ""
|
|
25
25
|
|
|
26
|
-
def load(self, project_dir: Path = None) -> bool:
|
|
27
|
-
"""Load configuration from .datasecops.yml and dbt_project.yml.
|
|
26
|
+
def load(self, project_dir: Path = None, connection_name: Optional[str] = None) -> bool:
|
|
27
|
+
"""Load configuration from .datasecops.yml and dbt_project.yml. `connection_name`
|
|
28
|
+
(a --connection option) overrides the file's and may stand in for a missing one."""
|
|
28
29
|
self.project_dir = project_dir or Path.cwd()
|
|
29
30
|
|
|
30
31
|
# Load .datasecops.yml
|
|
@@ -38,6 +39,7 @@ class Config:
|
|
|
38
39
|
app_database=raw.get("app_database", ""),
|
|
39
40
|
profile_name=raw.get("profile_name", ""),
|
|
40
41
|
cortex_connection=raw.get("cortex_connection", ""),
|
|
42
|
+
sandpit_name=raw.get("sandpit_name", ""),
|
|
41
43
|
dbt_engine=raw.get("dbt_engine", "dbt_core"),
|
|
42
44
|
dcm_enabled=raw.get("dcm_enabled", False),
|
|
43
45
|
dcm_security_dir=raw.get("dcm_security_dir", "security"),
|
|
@@ -53,6 +55,8 @@ class Config:
|
|
|
53
55
|
warning_line(f"Invalid dbt_engine '{self.datasecops.dbt_engine}' in .datasecops.yml — falling back to dbt_core")
|
|
54
56
|
self.datasecops.dbt_engine = "dbt_core"
|
|
55
57
|
|
|
58
|
+
if connection_name:
|
|
59
|
+
self.datasecops.connection_name = connection_name
|
|
56
60
|
if not self.datasecops.connection_name or not self.datasecops.app_database:
|
|
57
61
|
error_line(".datasecops.yml is missing connection_name or app_database.")
|
|
58
62
|
return False
|
|
@@ -17,7 +17,8 @@ from datasecops_cli.services.download_service import DownloadService
|
|
|
17
17
|
from datasecops_cli.services.skill_service import SkillService
|
|
18
18
|
from datasecops_cli.services.bootstrap_service import BootstrapService
|
|
19
19
|
from datasecops_cli.services.dcm_service import DcmService
|
|
20
|
-
from datasecops_cli.services import dcm_registry, model_commands
|
|
20
|
+
from datasecops_cli.services import dcm_registry, model_commands, pipeline_env
|
|
21
|
+
from datasecops_cli.models.project_config import DbtTarget
|
|
21
22
|
from datasecops_cli.menus.development import DevelopmentMenu
|
|
22
23
|
from datasecops_cli.menus.git_operations import GitOperationsMenu
|
|
23
24
|
from datasecops_cli.menus.downloads import DownloadsMenu
|
|
@@ -239,6 +240,55 @@ def dcm_pipelines_cmd(platform, names, force, projects, connection):
|
|
|
239
240
|
sf_service.close()
|
|
240
241
|
|
|
241
242
|
|
|
243
|
+
# ── pipeline commands ──────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@cli.group()
|
|
247
|
+
def pipeline():
|
|
248
|
+
"""Helpers for CI/CD pipelines."""
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@pipeline.command("env")
|
|
252
|
+
@click.option("--branch", "-b", default=None,
|
|
253
|
+
help="Branch being built: picks the dbt target mapped to it (a GitHub pull request's "
|
|
254
|
+
"<n>/merge ref resolves to its source branch)")
|
|
255
|
+
@click.option("--target", "-t", default=None, help="dbt target name (instead of --branch)")
|
|
256
|
+
@click.option("--connection", "-c", default=None, help="Snowflake connection name (overrides .datasecops.yml)")
|
|
257
|
+
@click.option("--format", "fmt", type=click.Choice(["auto", *pipeline_env.PLATFORMS]), default="auto",
|
|
258
|
+
show_default=True, help="auto: GitHub when $GITHUB_ENV is set, Azure DevOps when $TF_BUILD is set")
|
|
259
|
+
def pipeline_env_cmd(branch: Optional[str], target: Optional[str], connection: Optional[str], fmt: str):
|
|
260
|
+
"""Set TARGET, SF_ROLE, SF_WAREHOUSE and TARGET_SCHEMA from the app's dbt Targets.
|
|
261
|
+
|
|
262
|
+
GitHub Actions: appended to $GITHUB_ENV. Azure DevOps: set with ##vso logging commands.
|
|
263
|
+
Otherwise printed as KEY=value. Empty role / warehouse / schema values are left unset.
|
|
264
|
+
"""
|
|
265
|
+
if bool(branch) == bool(target):
|
|
266
|
+
raise click.UsageError("give exactly one of --branch or --target")
|
|
267
|
+
config = Config()
|
|
268
|
+
if not config.load(connection_name=connection):
|
|
269
|
+
sys.exit(1)
|
|
270
|
+
sf_service = SnowflakeService(config.datasecops)
|
|
271
|
+
try:
|
|
272
|
+
raw = sf_service.get_framework_config("PROJECT") or {}
|
|
273
|
+
except Exception as e: # noqa: BLE001 - connector errors
|
|
274
|
+
error_line(f"Failed to read the dbt targets from {config.datasecops.app_database}: {e}")
|
|
275
|
+
sys.exit(1)
|
|
276
|
+
finally:
|
|
277
|
+
sf_service.close()
|
|
278
|
+
targets = [DbtTarget(**{k: v for k, v in t.items() if k in DbtTarget.model_fields})
|
|
279
|
+
for t in raw.get("targets") or []]
|
|
280
|
+
try:
|
|
281
|
+
resolved = pipeline_env.resolve_target(targets, branch=branch, target=target)
|
|
282
|
+
except pipeline_env.TargetNotFound as e:
|
|
283
|
+
error_line(str(e))
|
|
284
|
+
sys.exit(1)
|
|
285
|
+
values = pipeline_env.env_values(resolved)
|
|
286
|
+
platform = pipeline_env.detect_platform() if fmt == "auto" else fmt
|
|
287
|
+
for line in pipeline_env.export(values, platform):
|
|
288
|
+
click.echo(line)
|
|
289
|
+
click.echo("dbt environment: " + ", ".join(f"{k}={v}" for k, v in values.items()), err=True)
|
|
290
|
+
|
|
291
|
+
|
|
242
292
|
# ── model commands ─────────────────────────────────────────────────────────
|
|
243
293
|
|
|
244
294
|
|
|
@@ -478,6 +528,9 @@ def _run_setup(project_dir: Path):
|
|
|
478
528
|
"MONITORIAL_ORBIT"
|
|
479
529
|
)
|
|
480
530
|
|
|
531
|
+
# --- Sandpit name (for your personal local-dev database) ---
|
|
532
|
+
sandpit_name = get_input_string("Enter your name (used to build your personal sandpit database, e.g. 'jon'): ")
|
|
533
|
+
|
|
481
534
|
# --- CoCo agent connection ---
|
|
482
535
|
cortex_connection = ""
|
|
483
536
|
if shutil.which("cortex") and connection_names:
|
|
@@ -588,46 +641,15 @@ def _run_setup(project_dir: Path):
|
|
|
588
641
|
# --- Ensure profiles.yml has the profile entry ---
|
|
589
642
|
if profile_name and matched_profile and not project_settings_loaded:
|
|
590
643
|
warning_line(f"Skipping profiles.yml for '{profile_name}': PROJECT framework config "
|
|
591
|
-
"was not loaded, so the
|
|
644
|
+
"was not loaded, so the local-dev role/warehouse aren't known")
|
|
592
645
|
elif profile_name and matched_profile:
|
|
593
|
-
import
|
|
594
|
-
from datasecops_cli.
|
|
595
|
-
from datasecops_cli.services.dbt_project_generator import generate_profiles_yml
|
|
646
|
+
from datasecops_cli.services.dbt_project_generator import write_profiles_yml
|
|
647
|
+
from datasecops_cli.services.snowflake_service import get_connection_params
|
|
596
648
|
|
|
597
649
|
profiles_dir = Path(project_settings.profile_dir).expanduser()
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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 {}
|
|
605
|
-
|
|
606
|
-
account_info = temp_sf.get_account_info()
|
|
607
|
-
account = account_info.get("ACCOUNT", account_info.get("account", ""))
|
|
608
|
-
user = account_info.get("USER_NAME", account_info.get("user_name", ""))
|
|
609
|
-
new_profiles_yaml = generate_profiles_yml(matched_profile, project_settings, account, user)
|
|
610
|
-
new_profiles = yaml.safe_load(new_profiles_yaml) or {}
|
|
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}")
|
|
650
|
+
connection_params = get_connection_params(connection_name)
|
|
651
|
+
status = write_profiles_yml(profiles_dir, profile_name, matched_profile, project_settings, connection_params, sandpit_name)
|
|
652
|
+
success_line(f"Profile '{profile_name}' {status} in {profiles_dir / 'profiles.yml'}")
|
|
631
653
|
except Exception as e:
|
|
632
654
|
warning_line(f"Could not resolve the project profile / write profiles.yml: {e}")
|
|
633
655
|
warning_line("You can set profile_name manually in .datasecops.yml")
|
|
@@ -643,6 +665,7 @@ def _run_setup(project_dir: Path):
|
|
|
643
665
|
"app_database": app_database,
|
|
644
666
|
"profile_name": profile_name,
|
|
645
667
|
"cortex_connection": cortex_connection,
|
|
668
|
+
"sandpit_name": sandpit_name,
|
|
646
669
|
}
|
|
647
670
|
write_datasecops_config(project_dir, config_data)
|
|
648
671
|
success_line(f".datasecops.yml written to {project_dir / '.datasecops.yml'}")
|
|
@@ -838,11 +861,11 @@ def _connect_and_load(config: Config, interactive: bool = True,
|
|
|
838
861
|
break
|
|
839
862
|
# Save the selected profile back to .datasecops.yml
|
|
840
863
|
if config.profile:
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
864
|
+
from datasecops_cli.utilities.yaml_utils import read_datasecops_config
|
|
865
|
+
config_data = read_datasecops_config(config.project_dir) or {}
|
|
866
|
+
config_data["connection_name"] = config.datasecops.connection_name
|
|
867
|
+
config_data["app_database"] = config.datasecops.app_database
|
|
868
|
+
config_data["profile_name"] = config.profile_name
|
|
846
869
|
write_datasecops_config(config.project_dir, config_data)
|
|
847
870
|
success_line(f"Saved profile_name '{config.profile_name}' to .datasecops.yml")
|
|
848
871
|
else:
|
|
@@ -850,11 +873,11 @@ def _connect_and_load(config: Config, interactive: bool = True,
|
|
|
850
873
|
info_line(f"Using default profile: {config.profile_name}")
|
|
851
874
|
elif not profile_was_set and len(config.all_profiles) == 1:
|
|
852
875
|
# Single profile auto-selected — persist it so future runs don't re-prompt
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
876
|
+
from datasecops_cli.utilities.yaml_utils import read_datasecops_config
|
|
877
|
+
config_data = read_datasecops_config(config.project_dir) or {}
|
|
878
|
+
config_data["connection_name"] = config.datasecops.connection_name
|
|
879
|
+
config_data["app_database"] = config.datasecops.app_database
|
|
880
|
+
config_data["profile_name"] = config.profile_name
|
|
858
881
|
write_datasecops_config(config.project_dir, config_data)
|
|
859
882
|
success_line(f"Saved profile_name '{config.profile_name}' to .datasecops.yml")
|
|
860
883
|
|
|
@@ -1274,7 +1297,7 @@ def _show_main_menu(profile_name: str, git_service: GitService = None,
|
|
|
1274
1297
|
|
|
1275
1298
|
def _run_bootstrap(config: Config):
|
|
1276
1299
|
"""Run the bootstrap command to initialise a new project."""
|
|
1277
|
-
from datasecops_cli.utilities.display import get_input_true_false, select_multiple_from_list
|
|
1300
|
+
from datasecops_cli.utilities.display import get_input_string, get_input_true_false, select_multiple_from_list
|
|
1278
1301
|
|
|
1279
1302
|
sf_service = _connect_and_load(config)
|
|
1280
1303
|
|
|
@@ -1285,6 +1308,15 @@ def _run_bootstrap(config: Config):
|
|
|
1285
1308
|
platform = config.source_control.source_control_platform.lower()
|
|
1286
1309
|
info_line(f"Platform: {platform}")
|
|
1287
1310
|
|
|
1311
|
+
# Sandpit name (for your personal local-dev database) — ask once, then persist
|
|
1312
|
+
if not config.datasecops.sandpit_name:
|
|
1313
|
+
from datasecops_cli.utilities.yaml_utils import read_datasecops_config, write_datasecops_config
|
|
1314
|
+
config.datasecops.sandpit_name = get_input_string(
|
|
1315
|
+
"Enter your name (used to build your personal sandpit database, e.g. 'jon'): ")
|
|
1316
|
+
config_data = read_datasecops_config(config.project_dir) or {}
|
|
1317
|
+
config_data["sandpit_name"] = config.datasecops.sandpit_name
|
|
1318
|
+
write_datasecops_config(config.project_dir, config_data)
|
|
1319
|
+
|
|
1288
1320
|
# Ask about optional steps
|
|
1289
1321
|
install_skills = get_input_true_false("Install AI coding skills?")
|
|
1290
1322
|
skill_targets = None
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/models/project_config.py
RENAMED
|
@@ -10,6 +10,7 @@ class DatasecopsConfig(BaseModel):
|
|
|
10
10
|
app_database: str = ""
|
|
11
11
|
profile_name: str = ""
|
|
12
12
|
cortex_connection: str = ""
|
|
13
|
+
sandpit_name: str = "" # personal name used to build your local-dev sandpit database (e.g. "jon")
|
|
13
14
|
dbt_engine: str = "dbt_core" # "dbt2" for dbt 2.0 (pip install dbt), "dbt_core" for dbt Core 1.x (pip install dbt-core + dbt-snowflake)
|
|
14
15
|
dcm_enabled: bool = False
|
|
15
16
|
dcm_security_dir: str = "security"
|
|
@@ -40,9 +41,6 @@ class ProjectSettings(BaseModel):
|
|
|
40
41
|
execution_mode: str = "dbt_cli"
|
|
41
42
|
dbt_engine: str = "dbt_core"
|
|
42
43
|
ai_agents_enabled: bool = False
|
|
43
|
-
ci_username: str = ""
|
|
44
|
-
ci_role: str = ""
|
|
45
|
-
ci_warehouse: str = ""
|
|
46
44
|
targets: list[DbtTarget] = Field(default_factory=lambda: [
|
|
47
45
|
DbtTarget(target_name="dev", branch_name="dev", target_role="DEVELOPERS", target_warehouse="DEV_WH", is_default=True),
|
|
48
46
|
DbtTarget(target_name="test", branch_name="test", target_role="DATAOPS_ADMIN", target_warehouse="DATAOPS_WH"),
|
|
@@ -46,6 +46,7 @@ _SAFE_PATTERNS = {
|
|
|
46
46
|
"profile name": re.compile(r"^[A-Za-z_][A-Za-z0-9_-]*$"),
|
|
47
47
|
"dbt version": re.compile(r"^[0-9]+(\.[0-9]+)*$"),
|
|
48
48
|
"warehouse name": re.compile(r"^[A-Za-z_][A-Za-z0-9_$]*$"),
|
|
49
|
+
"sandpit name": re.compile(r"^[A-Za-z0-9_]+$"),
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
|
|
@@ -193,74 +194,106 @@ vars:
|
|
|
193
194
|
)
|
|
194
195
|
|
|
195
196
|
|
|
196
|
-
def
|
|
197
|
+
def generate_local_dev_target(
|
|
197
198
|
profile: ProjectProfile,
|
|
198
199
|
project_settings: ProjectSettings,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
) -> str:
|
|
202
|
-
"""
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
200
|
+
connection_params: dict[str, Any],
|
|
201
|
+
sandpit_name: str,
|
|
202
|
+
) -> dict[str, Any]:
|
|
203
|
+
"""Build the `local-dev` profiles.yml output for a developer's own machine.
|
|
204
|
+
|
|
205
|
+
Connection identity (account/user/authenticator/private key/password) is taken as-is
|
|
206
|
+
from the developer's own Snowflake CLI connection (`~/.snowflake/connections.toml`) -
|
|
207
|
+
whatever auth method they already use to connect works for dbt too, with no CLI code
|
|
208
|
+
needing to know about it. `role`/`warehouse`/`schema` always come from the project's
|
|
209
|
+
dbt targets config, never from connections.toml. `database` is the developer's personal
|
|
210
|
+
sandpit database, built from their name so multiple developers never collide.
|
|
210
211
|
|
|
211
212
|
Args:
|
|
212
213
|
profile: The project profile.
|
|
213
|
-
project_settings: The project settings with targets
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
project_settings: The project settings with dbt targets.
|
|
215
|
+
connection_params: Raw params for the developer's Snowflake CLI connection, as
|
|
216
|
+
returned by `snowflake_service.get_connection_params()`.
|
|
217
|
+
sandpit_name: The developer's name, used to build their personal sandpit database.
|
|
216
218
|
|
|
217
219
|
Returns:
|
|
218
|
-
The
|
|
220
|
+
The `local-dev` output dict (to embed under `outputs.local-dev` in profiles.yml).
|
|
219
221
|
"""
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
"
|
|
225
|
-
"
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
222
|
+
target = project_settings.targets[0] if project_settings.targets else None
|
|
223
|
+
|
|
224
|
+
output: dict[str, Any] = {"type": "snowflake"}
|
|
225
|
+
for key in (
|
|
226
|
+
"account", "user", "authenticator", "password",
|
|
227
|
+
"private_key_path", "private_key", "private_key_passphrase", "token",
|
|
228
|
+
):
|
|
229
|
+
if connection_params.get(key):
|
|
230
|
+
output[key] = connection_params[key]
|
|
231
|
+
|
|
232
|
+
# Names can contain spaces, apostrophes, hyphens etc. (e.g. "O'Brien") - sanitize down to
|
|
233
|
+
# a safe database-identifier suffix rather than rejecting anything but a bare word.
|
|
234
|
+
sanitized_name = re.sub(r"[^A-Za-z0-9]+", "_", sandpit_name.strip()).strip("_").upper()
|
|
235
|
+
if not sanitized_name:
|
|
236
|
+
raise ValueError(f"sandpit name must contain at least one letter or digit: {sandpit_name!r}")
|
|
237
|
+
sandpit_suffix = _validate_safe(sanitized_name, "sandpit name")
|
|
238
|
+
output["role"] = target.target_role if target else "DEVELOPERS"
|
|
239
|
+
output["warehouse"] = target.target_warehouse if target else "DEV_WH"
|
|
240
|
+
output["database"] = f"{profile.target_database}SANDPIT_{sandpit_suffix}"
|
|
241
|
+
output["schema"] = target.target_schema if target else "PUBLIC"
|
|
242
|
+
output["threads"] = 4
|
|
243
|
+
return output
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def write_profiles_yml(
|
|
247
|
+
profiles_dir,
|
|
248
|
+
profile_name: str,
|
|
249
|
+
profile: ProjectProfile,
|
|
250
|
+
project_settings: ProjectSettings,
|
|
251
|
+
connection_params: dict[str, Any],
|
|
252
|
+
sandpit_name: str,
|
|
253
|
+
) -> str:
|
|
254
|
+
"""Add or update only the `local-dev` target for `profile_name` in
|
|
255
|
+
`<profiles_dir>/profiles.yml`, creating the `.dbt` folder and the file itself if
|
|
256
|
+
they don't already exist.
|
|
252
257
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
+
dbt reads `~/.dbt/profiles.yml` (per `project_settings.profile_dir`), never a copy
|
|
259
|
+
inside the dbt project folder. Deployment/CI targets (dev/test/prod) are never written
|
|
260
|
+
here - those belong to the native app's CI pipeline, not a developer's machine. Any
|
|
261
|
+
other targets already present in the file (added manually, or by a framework target
|
|
262
|
+
sharing this profile's name) are left untouched.
|
|
263
|
+
|
|
264
|
+
Returns "added" or "updated".
|
|
265
|
+
"""
|
|
266
|
+
from pathlib import Path
|
|
267
|
+
|
|
268
|
+
profiles_dir = Path(profiles_dir).expanduser()
|
|
269
|
+
profiles_path = profiles_dir / "profiles.yml"
|
|
270
|
+
|
|
271
|
+
existing_profiles: dict[str, Any] = {}
|
|
272
|
+
if profiles_path.exists():
|
|
273
|
+
existing_profiles = yaml.safe_load(profiles_path.read_text(encoding="utf-8")) or {}
|
|
274
|
+
|
|
275
|
+
local_dev_output = generate_local_dev_target(profile, project_settings, connection_params, sandpit_name)
|
|
276
|
+
|
|
277
|
+
if profile_name in existing_profiles:
|
|
278
|
+
entry = existing_profiles[profile_name]
|
|
279
|
+
entry.setdefault("outputs", {})
|
|
280
|
+
entry["outputs"]["local-dev"] = local_dev_output
|
|
281
|
+
# Always force the default target to local-dev: a bare `dbt run` with no --target
|
|
282
|
+
# must use the developer's own identity, never a deployment target that happened
|
|
283
|
+
# to be marked as the file's default (e.g. by a previous run or manual edit).
|
|
284
|
+
entry["target"] = "local-dev"
|
|
285
|
+
status = "updated"
|
|
286
|
+
else:
|
|
287
|
+
existing_profiles[profile_name] = {
|
|
258
288
|
"target": "local-dev",
|
|
259
|
-
"outputs":
|
|
289
|
+
"outputs": {"local-dev": local_dev_output},
|
|
260
290
|
}
|
|
261
|
-
|
|
291
|
+
status = "added"
|
|
262
292
|
|
|
263
|
-
|
|
293
|
+
profiles_dir.mkdir(parents=True, exist_ok=True)
|
|
294
|
+
profiles_path.write_text(
|
|
295
|
+
yaml.dump(existing_profiles, default_flow_style=False, sort_keys=False), encoding="utf-8")
|
|
296
|
+
return status
|
|
264
297
|
|
|
265
298
|
|
|
266
299
|
def generate_packages_yml(
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b5}/src/datasecops_cli/services/download_service.py
RENAMED
|
@@ -308,10 +308,11 @@ class DownloadService:
|
|
|
308
308
|
"""
|
|
309
309
|
from datasecops_cli.services.dbt_project_generator import (
|
|
310
310
|
generate_dbt_project_yml,
|
|
311
|
-
|
|
311
|
+
write_profiles_yml,
|
|
312
312
|
generate_gitignore,
|
|
313
313
|
)
|
|
314
314
|
from datasecops_cli.services.directory_scaffolder import scaffold_directories
|
|
315
|
+
from datasecops_cli.services.snowflake_service import get_connection_params
|
|
315
316
|
|
|
316
317
|
info_line("Scaffolding dbt project from framework configuration...")
|
|
317
318
|
|
|
@@ -324,13 +325,16 @@ class DownloadService:
|
|
|
324
325
|
write_file(dbt_project_dir / "dbt_project.yml", dbt_project_content)
|
|
325
326
|
success_line(f"dbt_project.yml written to {dbt_project_dir}")
|
|
326
327
|
|
|
327
|
-
#
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
328
|
+
# profiles.yml is never written inside the dbt project: dbt reads it from
|
|
329
|
+
# project_settings.profile_dir (default ~/.dbt). Only the local-dev target is ever
|
|
330
|
+
# written there, using the developer's own Snowflake CLI connection and personal
|
|
331
|
+
# sandpit database - deployment targets belong to the native app's CI pipeline.
|
|
332
|
+
connection_params = get_connection_params(self.sf.config.connection_name)
|
|
333
|
+
profiles_dir = Path(project_settings.profile_dir).expanduser()
|
|
334
|
+
status = write_profiles_yml(
|
|
335
|
+
profiles_dir, profile.profile_name, profile, project_settings,
|
|
336
|
+
connection_params, self.sf.config.sandpit_name)
|
|
337
|
+
success_line(f"profiles.yml {status} in {profiles_dir}")
|
|
334
338
|
|
|
335
339
|
# Generate .gitignore
|
|
336
340
|
write_file(dbt_project_dir / ".gitignore", generate_gitignore())
|