datasecops-cli 0.7.0b2__tar.gz → 0.7.0b4__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.0b4}/CHANGELOG.md +8 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/PKG-INFO +1 -1
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/docs/getting-started.md +23 -17
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/pyproject.toml +1 -1
- datasecops_cli-0.7.0b4/src/datasecops_cli/__init__.py +1 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/config.py +1 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/main.py +30 -48
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/models/project_config.py +1 -3
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/dbt_project_generator.py +89 -56
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/download_service.py +12 -8
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/snowflake_service.py +15 -0
- datasecops_cli-0.7.0b4/tests/test_dbt_project_generator_profiles.py +150 -0
- datasecops_cli-0.7.0b2/src/datasecops_cli/__init__.py +0 -1
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/.github/workflows/auto-tag.yml +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/.github/workflows/publish-cli.yml +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/.github/workflows/test.yml +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/.gitignore +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/AGENTS.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/DEVELOPMENT.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/LICENSE +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/README.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/docs/dcm.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/docs/git-operations.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/docs/legacy.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/docs/legacy_plan_of_action.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/docs/mcp-server.md +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/mcp-servers.json +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/scripts/vendor_model.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/setup.ps1 +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/setup.sh +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/menus/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/menus/configuration.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/menus/dcm_operations.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/menus/development.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/menus/downloads.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/menus/git_operations.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/models/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/models/git_helpers.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/bootstrap_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/dbt_runner.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/dcm_orchestrator.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/dcm_pipelines.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/dcm_registry.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/dcm_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/directory_scaffolder.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/git_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/linting_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/model_commands.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/skill_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/upstream_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/utilities/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/utilities/display.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/utilities/file_utils.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/utilities/mcp.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_mcp/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_mcp/__main__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_mcp/connection.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_mcp/server.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/convert.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/diagram.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/expand.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/extract.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/render.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/schema.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/scope.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/scripts.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/starter.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/templates/macros/role.sql +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/templates/macros/role_guard.sql +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/templates/macros/warehouse.sql +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/validate.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/__init__.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/test_config.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/test_dbt_runner.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/test_dcm_model.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/test_dcm_orchestrator.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/test_dcm_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/test_download_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/test_file_utils.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/test_linting_service.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/test_main.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/test_model_commands.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/test_models.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/test_version.py +0 -0
- {datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/tests/test_yaml_utils.py +0 -0
|
@@ -6,6 +6,14 @@ All notable changes to the DataSecOps CLI are documented in this file.
|
|
|
6
6
|
|
|
7
7
|
### Fixed
|
|
8
8
|
|
|
9
|
+
- **`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`).
|
|
10
|
+
- **`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.
|
|
11
|
+
- **`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.
|
|
12
|
+
|
|
13
|
+
## [0.7.0b2] - 2026-10-26
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
9
17
|
- **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
18
|
- **`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
19
|
- **`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.
|
|
@@ -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.0b4"
|
|
@@ -38,6 +38,7 @@ class Config:
|
|
|
38
38
|
app_database=raw.get("app_database", ""),
|
|
39
39
|
profile_name=raw.get("profile_name", ""),
|
|
40
40
|
cortex_connection=raw.get("cortex_connection", ""),
|
|
41
|
+
sandpit_name=raw.get("sandpit_name", ""),
|
|
41
42
|
dbt_engine=raw.get("dbt_engine", "dbt_core"),
|
|
42
43
|
dcm_enabled=raw.get("dcm_enabled", False),
|
|
43
44
|
dcm_security_dir=raw.get("dcm_security_dir", "security"),
|
|
@@ -478,6 +478,9 @@ def _run_setup(project_dir: Path):
|
|
|
478
478
|
"MONITORIAL_ORBIT"
|
|
479
479
|
)
|
|
480
480
|
|
|
481
|
+
# --- Sandpit name (for your personal local-dev database) ---
|
|
482
|
+
sandpit_name = get_input_string("Enter your name (used to build your personal sandpit database, e.g. 'jon'): ")
|
|
483
|
+
|
|
481
484
|
# --- CoCo agent connection ---
|
|
482
485
|
cortex_connection = ""
|
|
483
486
|
if shutil.which("cortex") and connection_names:
|
|
@@ -588,46 +591,15 @@ def _run_setup(project_dir: Path):
|
|
|
588
591
|
# --- Ensure profiles.yml has the profile entry ---
|
|
589
592
|
if profile_name and matched_profile and not project_settings_loaded:
|
|
590
593
|
warning_line(f"Skipping profiles.yml for '{profile_name}': PROJECT framework config "
|
|
591
|
-
"was not loaded, so the
|
|
594
|
+
"was not loaded, so the local-dev role/warehouse aren't known")
|
|
592
595
|
elif profile_name and matched_profile:
|
|
593
|
-
import
|
|
594
|
-
from datasecops_cli.
|
|
595
|
-
from datasecops_cli.services.dbt_project_generator import generate_profiles_yml
|
|
596
|
+
from datasecops_cli.services.dbt_project_generator import write_profiles_yml
|
|
597
|
+
from datasecops_cli.services.snowflake_service import get_connection_params
|
|
596
598
|
|
|
597
599
|
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}")
|
|
600
|
+
connection_params = get_connection_params(connection_name)
|
|
601
|
+
status = write_profiles_yml(profiles_dir, profile_name, matched_profile, project_settings, connection_params, sandpit_name)
|
|
602
|
+
success_line(f"Profile '{profile_name}' {status} in {profiles_dir / 'profiles.yml'}")
|
|
631
603
|
except Exception as e:
|
|
632
604
|
warning_line(f"Could not resolve the project profile / write profiles.yml: {e}")
|
|
633
605
|
warning_line("You can set profile_name manually in .datasecops.yml")
|
|
@@ -643,6 +615,7 @@ def _run_setup(project_dir: Path):
|
|
|
643
615
|
"app_database": app_database,
|
|
644
616
|
"profile_name": profile_name,
|
|
645
617
|
"cortex_connection": cortex_connection,
|
|
618
|
+
"sandpit_name": sandpit_name,
|
|
646
619
|
}
|
|
647
620
|
write_datasecops_config(project_dir, config_data)
|
|
648
621
|
success_line(f".datasecops.yml written to {project_dir / '.datasecops.yml'}")
|
|
@@ -838,11 +811,11 @@ def _connect_and_load(config: Config, interactive: bool = True,
|
|
|
838
811
|
break
|
|
839
812
|
# Save the selected profile back to .datasecops.yml
|
|
840
813
|
if config.profile:
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
814
|
+
from datasecops_cli.utilities.yaml_utils import read_datasecops_config
|
|
815
|
+
config_data = read_datasecops_config(config.project_dir) or {}
|
|
816
|
+
config_data["connection_name"] = config.datasecops.connection_name
|
|
817
|
+
config_data["app_database"] = config.datasecops.app_database
|
|
818
|
+
config_data["profile_name"] = config.profile_name
|
|
846
819
|
write_datasecops_config(config.project_dir, config_data)
|
|
847
820
|
success_line(f"Saved profile_name '{config.profile_name}' to .datasecops.yml")
|
|
848
821
|
else:
|
|
@@ -850,11 +823,11 @@ def _connect_and_load(config: Config, interactive: bool = True,
|
|
|
850
823
|
info_line(f"Using default profile: {config.profile_name}")
|
|
851
824
|
elif not profile_was_set and len(config.all_profiles) == 1:
|
|
852
825
|
# Single profile auto-selected — persist it so future runs don't re-prompt
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
826
|
+
from datasecops_cli.utilities.yaml_utils import read_datasecops_config
|
|
827
|
+
config_data = read_datasecops_config(config.project_dir) or {}
|
|
828
|
+
config_data["connection_name"] = config.datasecops.connection_name
|
|
829
|
+
config_data["app_database"] = config.datasecops.app_database
|
|
830
|
+
config_data["profile_name"] = config.profile_name
|
|
858
831
|
write_datasecops_config(config.project_dir, config_data)
|
|
859
832
|
success_line(f"Saved profile_name '{config.profile_name}' to .datasecops.yml")
|
|
860
833
|
|
|
@@ -1274,7 +1247,7 @@ def _show_main_menu(profile_name: str, git_service: GitService = None,
|
|
|
1274
1247
|
|
|
1275
1248
|
def _run_bootstrap(config: Config):
|
|
1276
1249
|
"""Run the bootstrap command to initialise a new project."""
|
|
1277
|
-
from datasecops_cli.utilities.display import get_input_true_false, select_multiple_from_list
|
|
1250
|
+
from datasecops_cli.utilities.display import get_input_string, get_input_true_false, select_multiple_from_list
|
|
1278
1251
|
|
|
1279
1252
|
sf_service = _connect_and_load(config)
|
|
1280
1253
|
|
|
@@ -1285,6 +1258,15 @@ def _run_bootstrap(config: Config):
|
|
|
1285
1258
|
platform = config.source_control.source_control_platform.lower()
|
|
1286
1259
|
info_line(f"Platform: {platform}")
|
|
1287
1260
|
|
|
1261
|
+
# Sandpit name (for your personal local-dev database) — ask once, then persist
|
|
1262
|
+
if not config.datasecops.sandpit_name:
|
|
1263
|
+
from datasecops_cli.utilities.yaml_utils import read_datasecops_config, write_datasecops_config
|
|
1264
|
+
config.datasecops.sandpit_name = get_input_string(
|
|
1265
|
+
"Enter your name (used to build your personal sandpit database, e.g. 'jon'): ")
|
|
1266
|
+
config_data = read_datasecops_config(config.project_dir) or {}
|
|
1267
|
+
config_data["sandpit_name"] = config.datasecops.sandpit_name
|
|
1268
|
+
write_datasecops_config(config.project_dir, config_data)
|
|
1269
|
+
|
|
1288
1270
|
# Ask about optional steps
|
|
1289
1271
|
install_skills = get_input_true_false("Install AI coding skills?")
|
|
1290
1272
|
skill_targets = None
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/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.0b4}/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())
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/snowflake_service.py
RENAMED
|
@@ -16,6 +16,21 @@ ALLOWED_PROCEDURES = frozenset({
|
|
|
16
16
|
})
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def get_connection_params(connection_name: str) -> dict[str, Any]:
|
|
20
|
+
"""Raw connection parameters for a named Snowflake CLI connection, read directly
|
|
21
|
+
from ~/.snowflake/connections.toml (no connection is established).
|
|
22
|
+
|
|
23
|
+
Returns an empty dict if the connection name isn't found or the config can't be read.
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
from snowflake.connector.config_manager import CONFIG_MANAGER
|
|
27
|
+
CONFIG_MANAGER.read_config()
|
|
28
|
+
connections = CONFIG_MANAGER["connections"]
|
|
29
|
+
return dict(connections.get(connection_name, {}))
|
|
30
|
+
except Exception:
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
|
|
19
34
|
class SnowflakeService:
|
|
20
35
|
"""Connects to Snowflake and calls native app API procedures."""
|
|
21
36
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Tests for the local-dev profiles.yml generation (sandpit database, connections.toml passthrough)."""
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from datasecops_cli.models.project_config import DbtTarget, ProjectProfile, ProjectSettings
|
|
7
|
+
from datasecops_cli.services.dbt_project_generator import generate_local_dev_target, write_profiles_yml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _profile(target_database: str = "SDS_") -> ProjectProfile:
|
|
11
|
+
return ProjectProfile(profile_name="anzco_sds", project_name="Anzco SDS", target_database=target_database)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _project_settings() -> ProjectSettings:
|
|
15
|
+
return ProjectSettings(targets=[
|
|
16
|
+
DbtTarget(target_name="dev", branch_name="dev", target_role="DEVELOPERS",
|
|
17
|
+
target_warehouse="DEV_WH", target_schema="PUBLIC", is_default=True),
|
|
18
|
+
DbtTarget(target_name="prod", branch_name="prod", target_role="DATAOPS_ADMIN",
|
|
19
|
+
target_warehouse="DATAOPS_WH", target_schema="PUBLIC"),
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_generate_local_dev_target_uses_connections_toml_identity():
|
|
24
|
+
connection_params = {
|
|
25
|
+
"account": "myaccount",
|
|
26
|
+
"user": "jon@dataengineers.co.nz",
|
|
27
|
+
"authenticator": "SNOWFLAKE_JWT",
|
|
28
|
+
"private_key_path": "/home/jon/.keys/rsa_key.p8",
|
|
29
|
+
"role": "accountadmin", # must be ignored - role always from dbt targets config
|
|
30
|
+
"warehouse": "COMPUTE_WH", # must be ignored - warehouse always from dbt targets config
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
output = generate_local_dev_target(_profile(), _project_settings(), connection_params, "jon")
|
|
34
|
+
|
|
35
|
+
assert output["type"] == "snowflake"
|
|
36
|
+
assert output["account"] == "myaccount"
|
|
37
|
+
assert output["user"] == "jon@dataengineers.co.nz"
|
|
38
|
+
assert output["authenticator"] == "SNOWFLAKE_JWT"
|
|
39
|
+
assert output["private_key_path"] == "/home/jon/.keys/rsa_key.p8"
|
|
40
|
+
# role/warehouse/schema always come from the project's dbt targets config, never connections.toml
|
|
41
|
+
assert output["role"] == "DEVELOPERS"
|
|
42
|
+
assert output["warehouse"] == "DEV_WH"
|
|
43
|
+
assert output["schema"] == "PUBLIC"
|
|
44
|
+
assert output["threads"] == 4
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_generate_local_dev_target_builds_sandpit_database():
|
|
48
|
+
output = generate_local_dev_target(_profile("SDS_"), _project_settings(), {}, "jon")
|
|
49
|
+
assert output["database"] == "SDS_SANDPIT_JON"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_generate_local_dev_target_sanitizes_name_with_spaces_and_punctuation():
|
|
53
|
+
"""Real names (spaces, apostrophes, hyphens) are sanitized into a safe database
|
|
54
|
+
identifier suffix rather than rejected outright."""
|
|
55
|
+
output = generate_local_dev_target(_profile(), _project_settings(), {}, "O'Brien Jr.")
|
|
56
|
+
assert output["database"] == "SDS_SANDPIT_O_BRIEN_JR"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_generate_local_dev_target_rejects_name_with_no_alphanumerics():
|
|
60
|
+
with pytest.raises(ValueError):
|
|
61
|
+
generate_local_dev_target(_profile(), _project_settings(), {}, "---")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_generate_local_dev_target_omits_missing_connection_fields():
|
|
65
|
+
output = generate_local_dev_target(_profile(), _project_settings(), {"account": "acct"}, "jon")
|
|
66
|
+
assert output["account"] == "acct"
|
|
67
|
+
assert "private_key_path" not in output
|
|
68
|
+
assert "password" not in output
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_write_profiles_yml_creates_folder_and_file(tmp_path):
|
|
72
|
+
profiles_dir = tmp_path / ".dbt"
|
|
73
|
+
assert not profiles_dir.exists()
|
|
74
|
+
|
|
75
|
+
status = write_profiles_yml(
|
|
76
|
+
profiles_dir, "anzco_sds", _profile(), _project_settings(), {"account": "acct"}, "jon")
|
|
77
|
+
|
|
78
|
+
assert status == "added"
|
|
79
|
+
profiles_path = profiles_dir / "profiles.yml"
|
|
80
|
+
assert profiles_path.exists()
|
|
81
|
+
|
|
82
|
+
data = yaml.safe_load(profiles_path.read_text(encoding="utf-8"))
|
|
83
|
+
assert data["anzco_sds"]["target"] == "local-dev"
|
|
84
|
+
assert data["anzco_sds"]["outputs"]["local-dev"]["database"] == "SDS_SANDPIT_JON"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_write_profiles_yml_only_touches_local_dev(tmp_path):
|
|
88
|
+
profiles_dir = tmp_path / ".dbt"
|
|
89
|
+
profiles_dir.mkdir(parents=True)
|
|
90
|
+
profiles_path = profiles_dir / "profiles.yml"
|
|
91
|
+
profiles_path.write_text(yaml.dump({
|
|
92
|
+
"anzco_sds": {
|
|
93
|
+
"target": "local-dev",
|
|
94
|
+
"outputs": {
|
|
95
|
+
"local-dev": {"type": "snowflake", "account": "old", "database": "OLD_DB"},
|
|
96
|
+
"dev": {"type": "snowflake", "account": "ci", "database": "SDS_DEV"},
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
}), encoding="utf-8")
|
|
100
|
+
|
|
101
|
+
status = write_profiles_yml(
|
|
102
|
+
profiles_dir, "anzco_sds", _profile(), _project_settings(), {"account": "acct"}, "jon")
|
|
103
|
+
|
|
104
|
+
assert status == "updated"
|
|
105
|
+
data = yaml.safe_load(profiles_path.read_text(encoding="utf-8"))
|
|
106
|
+
# local-dev was refreshed
|
|
107
|
+
assert data["anzco_sds"]["outputs"]["local-dev"]["account"] == "acct"
|
|
108
|
+
assert data["anzco_sds"]["outputs"]["local-dev"]["database"] == "SDS_SANDPIT_JON"
|
|
109
|
+
# the CI/deployment "dev" target is untouched
|
|
110
|
+
assert data["anzco_sds"]["outputs"]["dev"] == {"type": "snowflake", "account": "ci", "database": "SDS_DEV"}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_write_profiles_yml_forces_default_target_to_local_dev(tmp_path):
|
|
114
|
+
"""A bare `dbt run` must always use local-dev, even if the file's default target was
|
|
115
|
+
previously left pointing at a deployment target (manually, or by an older CLI version)."""
|
|
116
|
+
profiles_dir = tmp_path / ".dbt"
|
|
117
|
+
profiles_dir.mkdir(parents=True)
|
|
118
|
+
profiles_path = profiles_dir / "profiles.yml"
|
|
119
|
+
profiles_path.write_text(yaml.dump({
|
|
120
|
+
"anzco_sds": {
|
|
121
|
+
"target": "dev",
|
|
122
|
+
"outputs": {
|
|
123
|
+
"local-dev": {"type": "snowflake", "account": "old"},
|
|
124
|
+
"dev": {"type": "snowflake", "account": "ci"},
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}), encoding="utf-8")
|
|
128
|
+
|
|
129
|
+
write_profiles_yml(profiles_dir, "anzco_sds", _profile(), _project_settings(), {"account": "acct"}, "jon")
|
|
130
|
+
|
|
131
|
+
data = yaml.safe_load(profiles_path.read_text(encoding="utf-8"))
|
|
132
|
+
assert data["anzco_sds"]["target"] == "local-dev"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_write_profiles_yml_adds_new_profile_without_disturbing_others(tmp_path):
|
|
136
|
+
profiles_dir = tmp_path / ".dbt"
|
|
137
|
+
profiles_dir.mkdir(parents=True)
|
|
138
|
+
profiles_path = profiles_dir / "profiles.yml"
|
|
139
|
+
profiles_path.write_text(yaml.dump({
|
|
140
|
+
"other_profile": {"target": "local-dev", "outputs": {"local-dev": {"type": "snowflake", "account": "x"}}},
|
|
141
|
+
}), encoding="utf-8")
|
|
142
|
+
|
|
143
|
+
status = write_profiles_yml(
|
|
144
|
+
profiles_dir, "anzco_sds", _profile(), _project_settings(), {"account": "acct"}, "jon")
|
|
145
|
+
|
|
146
|
+
assert status == "added"
|
|
147
|
+
data = yaml.safe_load(profiles_path.read_text(encoding="utf-8"))
|
|
148
|
+
assert "other_profile" in data
|
|
149
|
+
assert "anzco_sds" in data
|
|
150
|
+
assert data["anzco_sds"]["outputs"]["local-dev"]["database"] == "SDS_SANDPIT_JON"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.7.0b2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/menus/dcm_operations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/menus/git_operations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/bootstrap_service.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/dcm_orchestrator.py
RENAMED
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/dcm_pipelines.py
RENAMED
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/dcm_registry.py
RENAMED
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/dcm_service.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/git_service.py
RENAMED
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/linting_service.py
RENAMED
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/model_commands.py
RENAMED
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/skill_service.py
RENAMED
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/services/upstream_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/utilities/file_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_cli/utilities/yaml_utils.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
|
{datasecops_cli-0.7.0b2 → datasecops_cli-0.7.0b4}/src/datasecops_model/templates/macros/role.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|