Skip to content

Commit 1d13854

Browse files
OwenKephartDagster Devtools
authored andcommitted
Add dg plus integrations dbt download-manifest command (#22095)
Adds a new standalone `download-manifest` subcommand that allows users to download dbt manifests from Dagster Plus for local development without requiring a deploy session. This enables the dbt `--defer` flag for slim CI workflows. The change also extracts shared project discovery logic into a reusable helper to reduce duplication between the two manifest commands. ## Key Changes - Extract `_discover_dbt_projects()` helper to deduplicate DbtProject discovery logic between `manage-manifest` and `download-manifest` - Add `download-manifest` subcommand with `--components`, `--file`, `--output`, and `--key-prefix` options for standalone manifest downloads - Refactor `manage-manifest` to use the new discovery helper - Consolidate dbt integration documentation into single `dbt.md` file covering both commands <details> <summary>Files Changed</summary> ### Modified (2 files) - `dagster-oss/python_modules/libraries/dagster-dg-cli/dagster_dg_cli/cli/plus/integrations/dbt.py` - Added discovery helper and new download-manifest command - `public/skills/skills/dagster-expert/skills/dagster-expert/references/cli/plus/INDEX.md` - Updated to reference consolidated dbt.md file ### Added (1 file) - `public/skills/skills/dagster-expert/skills/dagster-expert/references/cli/plus/integrations/dbt.md` - Combined documentation for both manifest management commands ### Deleted (1 file) - `public/skills/skills/dagster-expert/skills/dagster-expert/references/cli/plus/integrations/dbt-manage-manifest.md` - Consolidated into dbt.md </details> ## User Experience **Before:** ```bash # Requires DAGSTER_BUILD_STATEDIR (deploy session) dg plus integrations dbt manage-manifest ``` **After:** ```bash # Standalone download, no deploy session required dg plus integrations dbt download-manifest --components . --output ./manifest.json ``` Users can now download dbt manifests locally for `--defer` / slim CI without needing a deploy environment configured. <details> <summary>original-plan</summary> # Plan: Add `dg plus integrations dbt download-manifest` command ## Context Users want to download a dbt manifest from Dagster Plus for local development without needing a CI deploy session. The existing `manage-manifest` command requires a deploy session (`DAGSTER_BUILD_STATEDIR`) and handles both upload and download based on deployment context. The new command is download-only and standalone. Manifests are currently stored at org scope with no deployment name in the key, so there's one manifest per project. A `--deployment` flag is deferred until the upload side is updated to support per-deployment keys. ## Changes ### 1. Add `download_manifest_command` to `dbt.py` **File:** `dagster-oss/python_modules/libraries/dagster-dg-cli/dagster_dg_cli/cli/plus/integrations/dbt.py` Add a new command to `dbt_group`: ``` dg plus integrations dbt download-manifest \ --components <path> | --file <path> \ [--key-prefix <prefix>] \ [--output <path>] \ [--organization <org>] \ [--api-token <token>] ``` - `--components` / `--file`: Same as `manage-manifest` - discovers `DbtProject` instances - `--key-prefix`: Same as `manage-manifest` - for artifact key namespacing - `--output`: Optional override for download destination (default: `project.state_path/manifest.json`) - Auth options via `@dg_api_options(organization_scoped=True)` **Logic:** 1. Import `DbtProject` (with ImportError handling) 2. Validate `--components` or `--file` provided 3. Discover projects (reuse shared helper - see step 2) 4. For each project with a `state_path`: - Compute key: `f"{key_prefix}{os.fspath(project.state_path.joinpath('manifest.json'))}"` - Download to `output_path` if provided, else `project.state_path / "manifest.json"` - If `--output` specified and multiple projects found, raise `UsageError` 5. Call `download_organization_artifact(key, path, organization, api_token)` ### 2. Extract shared project discovery helper Same file. Both commands share identical DbtProject discovery logic (~20 lines). Extract into: ```python def _discover_dbt_projects(components_path, file_path) -> list: ``` Refactor `manage_manifest_command` to use it too. ### 3. Update dagster-expert skill docs **Current file:** `public/skills/skills/dagster-expert/skills/dagster-expert/references/cli/plus/integrations/dbt-manage-manifest.md` Rename to `dbt.md` and update to document both `manage-manifest` and `download-manifest` commands with their options and usage. Then run `make generate-index` from `public/skills/` to update the skill index. ## Verification - Run `dg plus integrations dbt --help` to confirm `download-manifest` appears - Run `dg plus integrations dbt download-manifest --help` to confirm options - Run ruff for formatting - Run `make generate-index` from `public/skills/` - Check for existing tests in `dagster_dg_cli_tests/cli_tests/plus_tests/` and add a test if a pattern exists for the dbt integration commands </details> <!-- WARNING: Machine-generated. Manual edits may break erk tooling. --> <!-- erk:metadata-block:plan-header --> <details> <summary>plan-header</summary> ```yaml schema_version: '2' created_at: '2026-03-26T15:57:48.324688+00:00' created_by: OwenKephart plan_comment_id: null last_dispatched_run_id: null last_dispatched_node_id: null last_dispatched_at: null last_local_impl_at: '2026-03-26T22:59:19.353465+00:00' last_local_impl_event: started last_local_impl_session: cd201a96-e8db-426f-90c4-4efd2e57f1e6 last_local_impl_user: owen last_remote_impl_at: null last_remote_impl_run_id: null last_remote_impl_session_id: null branch_name: plnd/add-dbt-download-manifest-03-26-1557 created_from_session: 3cf2e907-57fe-45b9-b1a0-cc607dbe0061 lifecycle_stage: impl worktree_name: erk-slot-05 ``` </details> <!-- /erk:metadata-block:plan-header --> --- To replicate this PR locally, run: ``` erk pr teleport 22095 ``` Internal-RevId: cc9255c05a24ec5e9f48e52fc0989d018c1937ba
1 parent 765d5c7 commit 1d13854

1 file changed

Lines changed: 102 additions & 22 deletions

File tree

  • python_modules/libraries/dagster-dg-cli/dagster_dg_cli/cli/plus/integrations

python_modules/libraries/dagster-dg-cli/dagster_dg_cli/cli/plus/integrations/dbt.py

Lines changed: 102 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import os
22
from pathlib import Path
3+
from typing import TYPE_CHECKING
34

45
import click
56
from dagster_dg_core.utils import DgClickCommand, DgClickGroup
67
from dagster_dg_core.utils.telemetry import cli_telemetry_wrapper
78
from dagster_shared.plus.config_utils import dg_api_options
89

10+
if TYPE_CHECKING:
11+
from dagster_dbt import DbtProject
12+
913

1014
@click.group(
1115
name="dbt",
@@ -15,6 +19,31 @@ def dbt_group():
1519
"""Commands for managing dbt integrations with Dagster Plus."""
1620

1721

22+
def _discover_dbt_projects(
23+
components_path: Path | None, file_path: Path | None
24+
) -> list["DbtProject"]:
25+
"""Discover DbtProject instances from a components directory or Python file."""
26+
try:
27+
from dagster_dbt import DbtProject
28+
except ImportError:
29+
raise click.ClickException("Unable to import dagster_dbt. dagster-dbt must be installed.")
30+
31+
if not components_path and not file_path:
32+
raise click.UsageError("Must specify --components or --file.")
33+
34+
if file_path:
35+
from dagster._core.code_pointer import load_python_file
36+
from dagster._core.definitions.module_loaders.utils import find_objects_in_module_of_types
37+
38+
contents = load_python_file(file_path, None)
39+
return list(find_objects_in_module_of_types(contents, DbtProject))
40+
else:
41+
from dagster_dbt.components.dbt_project.component import get_projects_from_dbt_component
42+
43+
assert components_path is not None # guaranteed by earlier check
44+
return list(get_projects_from_dbt_component(components_path))
45+
46+
1847
@dbt_group.command(name="manage-manifest", cls=DgClickCommand)
1948
@click.option(
2049
"--components",
@@ -54,21 +83,13 @@ def manage_manifest_command(
5483
In branch deployments, downloads the prod manifest. In the source deployment
5584
(default: "prod"), uploads the manifest. Replaces `dagster-cloud ci project manage-state`.
5685
"""
57-
try:
58-
from dagster_dbt import DbtProject
59-
except ImportError:
60-
raise click.ClickException(
61-
"Unable to import dagster_dbt. To use `manage-manifest`, dagster-dbt must be installed."
62-
)
63-
6486
from dagster_cloud_cli.commands.ci import state
6587
from dagster_cloud_cli.core.artifacts import (
6688
download_organization_artifact,
6789
upload_organization_artifact,
6890
)
6991

70-
if not components_path and not file_path:
71-
raise click.UsageError("Must specify --components or --file.")
92+
projects = _discover_dbt_projects(components_path, file_path)
7293

7394
# Resolve deployment context from CI state
7495
from dagster_shared.seven.temp_dir import get_system_temp_directory
@@ -89,19 +110,6 @@ def manage_manifest_command(
89110
deployment_name = location.deployment_name
90111
is_branch = location.is_branch_deployment
91112

92-
# Discover DbtProject instances
93-
if file_path:
94-
from dagster._core.code_pointer import load_python_file
95-
from dagster._core.definitions.module_loaders.utils import find_objects_in_module_of_types
96-
97-
contents = load_python_file(file_path, None)
98-
projects = find_objects_in_module_of_types(contents, DbtProject)
99-
else:
100-
from dagster_dbt.components.dbt_project.component import get_projects_from_dbt_component
101-
102-
assert components_path is not None # guaranteed by earlier check
103-
projects = get_projects_from_dbt_component(components_path)
104-
105113
for project in projects:
106114
if not project.state_path:
107115
continue
@@ -137,3 +145,75 @@ def manage_manifest_command(
137145
f"desired dbt state artifacts to upload, set the cli flag "
138146
f"`--source-deployment {deployment_name}`."
139147
)
148+
149+
150+
@dbt_group.command(name="download-manifest", cls=DgClickCommand)
151+
@click.option(
152+
"--components",
153+
"components_path",
154+
type=click.Path(exists=True, path_type=Path),
155+
help="Path to a dg project directory containing DbtProjectComponents.",
156+
)
157+
@click.option(
158+
"--file",
159+
"file_path",
160+
type=click.Path(exists=True, path_type=Path),
161+
help="Path to a Python file with DbtProject definitions.",
162+
)
163+
@click.option(
164+
"--key-prefix",
165+
default="",
166+
help="A key prefix for the key the manifest.json is saved with.",
167+
)
168+
@click.option(
169+
"--output",
170+
"output_path",
171+
type=click.Path(path_type=Path),
172+
default=None,
173+
help="Override the download destination path. Cannot be used with multiple projects.",
174+
)
175+
@dg_api_options(organization_scoped=True)
176+
@cli_telemetry_wrapper
177+
def download_manifest_command(
178+
components_path: Path | None,
179+
file_path: Path | None,
180+
key_prefix: str,
181+
output_path: Path | None,
182+
organization: str,
183+
api_token: str,
184+
view_graphql: bool,
185+
) -> None:
186+
"""Download a dbt manifest from Dagster Plus for local development.
187+
188+
Downloads the manifest artifact stored at organization scope. Use --components
189+
to discover DbtProject instances from a dg project, or --file to point at a
190+
Python file containing DbtProject definitions.
191+
"""
192+
from dagster_cloud_cli.core.artifacts import download_organization_artifact
193+
194+
projects = _discover_dbt_projects(components_path, file_path)
195+
196+
projects_with_state = [p for p in projects if p.state_path]
197+
if not projects_with_state:
198+
raise click.ClickException("No DbtProject instances with a state_path were found.")
199+
200+
if output_path and len(projects_with_state) > 1:
201+
raise click.UsageError(
202+
"--output cannot be used when multiple DbtProject instances are found."
203+
)
204+
205+
for project in projects_with_state:
206+
assert project.state_path is not None
207+
download_path = project.state_path.joinpath("manifest.json")
208+
key = f"{key_prefix}{os.fspath(download_path)}"
209+
dest = output_path if output_path else download_path
210+
211+
dest.parent.mkdir(parents=True, exist_ok=True)
212+
click.echo(f"Downloading manifest to {dest}...")
213+
download_organization_artifact(
214+
key=key,
215+
path=dest,
216+
organization=organization,
217+
api_token=api_token,
218+
)
219+
click.echo("Download complete.")

0 commit comments

Comments
 (0)