Skip to content

Commit 2550669

Browse files
authored
Merge pull request #8 from trouze/new-feats
New feats
2 parents 71be5e0 + d7bd879 commit 2550669

22 files changed

Lines changed: 911 additions & 150 deletions

CLAUDE.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,9 @@ dbtp/
6767
│ │ └── dimension_values.rs
6868
│ └── core/ # Shared infrastructure
6969
│ ├── mod.rs
70-
│ ├── config.rs # Profile loading/saving, env var fallback, config.toml
70+
│ ├── config.rs # Profile loading/saving, env var fallback, config.toml, interactive helpers
7171
│ ├── error.rs # DbtpError enum (Http, Config, GraphQL, Api, Io, Json, Arrow)
72+
│ ├── resolve.rs # Name-or-ID resolution for projects and environments
7273
│ ├── rest_client.rs # REST client: v2/v3 URL routing, auth, pagination, envelope unwrapping
7374
│ └── graphql_client.rs # GraphQL client for Discovery + Semantic Layer
7475
├── openapi/ # Reference OpenAPI specs (not used at build time)
@@ -89,6 +90,8 @@ dbtp/
8990
- **Auto-pagination**: `RestClient::paginate_v2` / `paginate_v3` loop through offset-based pages (100 per request) until `total_count` is reached or `--limit` is satisfied.
9091
- **Envelope unwrapping**: dbt Cloud REST APIs return `{ data, status, extra }`. The client strips this automatically via `unwrap_envelope()`.
9192
- **Compact mode**: A `compact` output format provides single-line JSON and uses `compact_*` helpers in `api/admin/mod.rs` to reduce verbose API responses to essential fields.
93+
- **Interactive configure**: `dbtp configure` detects whether stdin is a TTY. In interactive terminals, it fetches projects and environments via the API and offers a numbered selection menu. The `--non-interactive` flag or a non-TTY stdin falls through to plain ID prompts. API errors during interactive configure are handled gracefully with a fallback to manual entry.
94+
- **Name-or-ID resolution**: `project_id` and `environment_id` accept names or numeric IDs everywhere (config, env vars, CLI flags). `core/resolve.rs` tries `parse::<u64>()` first (zero API calls), then falls back to a list+filter API call with case-insensitive exact matching.
9295

9396
## Building and running
9497

@@ -104,7 +107,9 @@ The release profile enables `strip = true`, `lto = true`, `codegen-units = 1` fo
104107

105108
Config lives at `~/.config/dbtp/config.toml` (macOS) via the `directories` crate's `ProjectDirs::from("com", "dbt-labs", "dbtp")`.
106109

107-
Precedence: CLI flags > env vars (`DBTP_TOKEN`, `DBTP_HOST`, `DBTP_ACCOUNT_ID`, `DBTP_ENVIRONMENT_ID`) > profile config > defaults.
110+
Precedence: CLI flags > env vars (`DBTP_TOKEN`, `DBTP_HOST`, `DBTP_ACCOUNT_ID`, `DBTP_PROJECT_ID`, `DBTP_ENVIRONMENT_ID`) > profile config > defaults.
111+
112+
`project_id` and `environment_id` accept either a numeric ID or a name. Names are resolved to numeric IDs via a list+filter API call (case-insensitive exact match). Numeric IDs skip the API call entirely. Resolution happens in `main.rs` before command dispatch, so all downstream code receives numeric IDs.
108113

109114
## Key dependencies
110115

README.md

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
A fast, ergonomic command-line interface for the [dbt Cloud](https://www.getdbt.com/product/dbt-cloud) platform APIs. Manage accounts, projects, environments, jobs, and runs. Browse your dbt DAG through the Discovery API. Query metrics through the Semantic Layer. All from your terminal.
44

55
```
6-
dbtp jobs trigger 48213 --cause "deploy from CI"
7-
dbtp runs wait 901244 --interval 15
6+
dbtp jobs trigger 48213 --cause "deploy from CI" --wait
87
dbtp models health orders
98
dbtp metrics query revenue --group-by metric_time --grain MONTH
109
```
@@ -17,7 +16,7 @@ dbtp metrics query revenue --group-by metric_time --grain MONTH
1716
curl -fsSL https://raw.githubusercontent.com/trouze/dbtp/main/install.sh | bash
1817
```
1918

20-
This downloads the latest prebuilt binary from [GitHub Releases](https://github.com/trouze/dbtp/releases) and installs it to `/usr/local/bin`. Set `DBTP_INSTALL_DIR` to customize the location.
19+
This downloads the latest prebuilt binary from [GitHub Releases](https://github.com/trouze/dbtp/releases) and installs it to `~/.local/bin`. Set `DBTP_INSTALL_DIR` to customize the location.
2120

2221
### From source via Git (requires Rust)
2322

@@ -41,7 +40,7 @@ cargo install --path .
4140
dbtp configure
4241
```
4342

44-
You'll be prompted for your dbt Cloud host, API token, account ID, and (optionally) an environment ID for Discovery/Semantic Layer commands. Configuration is saved to `~/.config/dbtp/config.toml`.
43+
You'll be prompted for your dbt Cloud host, API token, account ID, and optionally a project and environment ID. When running in an interactive terminal, `configure` offers a numbered picker for projects and environments so you don't have to look up IDs. Use `--non-interactive` to disable the picker. Configuration is saved to `~/.config/dbtp/config.toml`.
4544

4645
### 2. Verify access
4746

@@ -54,8 +53,11 @@ dbtp accounts list
5453
```bash
5554
dbtp projects list
5655
dbtp jobs list --project-id 12345
56+
dbtp jobs list --project-id Analytics # name resolution
57+
dbtp environments list # uses configured project
5758
dbtp runs show 901244 -o json
5859
dbtp models list --environment-id 67890
60+
dbtp models list --environment-id Production # name resolution
5961
```
6062

6163
## Configuration
@@ -73,12 +75,15 @@ output = "table"
7375
host = "https://cloud.getdbt.com"
7476
token = "dbtc_..."
7577
account_id = 51798
78+
project_id = "12345" # numeric ID or project name
79+
environment_id = "67890" # numeric ID or environment name
7680

7781
[profile.staging]
7882
host = "https://emea.dbt.com"
7983
token = "dbtc_..."
8084
account_id = 99999
81-
environment_id = 67890
85+
project_id = "Analytics" # resolved to numeric ID at runtime
86+
environment_id = "Production"
8287
```
8388

8489
### Environment variables
@@ -88,7 +93,8 @@ environment_id = 67890
8893
| `DBTP_HOST` | dbt Cloud host URL |
8994
| `DBTP_TOKEN` | API token (service token or personal access token) |
9095
| `DBTP_ACCOUNT_ID` | Default account ID |
91-
| `DBTP_ENVIRONMENT_ID` | Default environment ID (Discovery / Semantic Layer) |
96+
| `DBTP_PROJECT_ID` | Default project ID or name |
97+
| `DBTP_ENVIRONMENT_ID` | Default environment ID or name (Discovery / Semantic Layer) |
9298

9399
### Precedence
94100

@@ -170,8 +176,7 @@ dbtp jobs list -o compact | jq '.[].name'
170176
### Trigger a job and wait for completion
171177

172178
```bash
173-
dbtp jobs trigger 48213 --cause "nightly refresh"
174-
dbtp runs wait 901244 --interval 10 --timeout 1800
179+
dbtp jobs trigger 48213 --cause "nightly refresh" --wait
175180
```
176181

177182
### Inspect a failed run
@@ -233,10 +238,15 @@ dbtp artifacts get 901244 run_results.json
233238
| `--host <url>` | | dbt Cloud host URL |
234239
| `--token <token>` | | API token |
235240
| `--account-id <id>` | | dbt Cloud account ID |
236-
| `--environment-id <id>` | | Environment ID (Discovery / Semantic Layer) |
241+
| `--project-id <id-or-name>` | | Project ID or name |
242+
| `--environment-id <id-or-name>` | | Environment ID or name (Discovery / Semantic Layer) |
237243
| `--verbose` | `-v` | Enable verbose output |
238244
| `--query <jmespath>` | | JMESPath expression to filter JSON output |
239245

246+
### Name resolution
247+
248+
`--project-id` and `--environment-id` accept either a numeric ID or a project/environment **name**. When a name is given, the CLI resolves it to the numeric ID via a list API call with case-insensitive exact matching. Numeric IDs bypass the API call entirely (zero overhead). The same applies to `DBTP_PROJECT_ID`, `DBTP_ENVIRONMENT_ID`, and the config file values.
249+
240250
## Shell Completions
241251

242252
Generate tab-completion scripts for your shell:
@@ -260,20 +270,8 @@ dbtp completion powershell > _dbtp.ps1
260270
- **API version routing** is handled automatically. The v2/v3 split in dbt Cloud's API is invisible to users — each command knows which version to use.
261271
- **Response envelope unwrapping** — dbt Cloud APIs return responses wrapped in `{ data, status, extra }`. The CLI strips the envelope and presents `data` directly.
262272
- **Auto-pagination**`list` commands transparently paginate through all results. Use `--limit` to cap the total.
263-
- **Waiters**`dbtp runs wait` polls until a run reaches a terminal state, printing status transitions as they happen.
264-
265-
## Releasing
266-
267-
Releases are automated via GitHub Actions. To cut a release:
268-
269-
```bash
270-
# Update version in Cargo.toml, then:
271-
git commit -am "release: v0.1.0"
272-
git tag v0.1.0
273-
git push && git push --tags
274-
```
273+
- **Waiters**`dbtp jobs trigger --wait` polls until a triggered run reaches a terminal state, printing status transitions as they happen.
275274

276-
The [release workflow](.github/workflows/release.yml) builds binaries for macOS (arm64 + x86_64) and Linux (x86_64 + arm64), then creates a GitHub Release with all artifacts and checksums.
277275

278276
## License
279277

install.sh

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ set -euo pipefail
33

44
REPO="trouze/dbtp"
55
BINARY="dbtp"
6-
INSTALL_DIR="${DBTP_INSTALL_DIR:-/usr/local/bin}"
6+
INSTALL_DIR="${DBTP_INSTALL_DIR:-$HOME/.local/bin}"
77

88
get_latest_version() {
99
curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \
@@ -45,15 +45,23 @@ main() {
4545

4646
curl -fsSL "${url}" | tar xz -C "${tmpdir}"
4747

48-
if [ -w "${INSTALL_DIR}" ]; then
49-
mv "${tmpdir}/${BINARY}" "${INSTALL_DIR}/${BINARY}"
50-
else
51-
echo "Installing to ${INSTALL_DIR} (requires sudo)..."
52-
sudo mv "${tmpdir}/${BINARY}" "${INSTALL_DIR}/${BINARY}"
53-
fi
54-
48+
mkdir -p "${INSTALL_DIR}"
49+
mv "${tmpdir}/${BINARY}" "${INSTALL_DIR}/${BINARY}"
5550
chmod +x "${INSTALL_DIR}/${BINARY}"
51+
5652
echo "Installed ${BINARY} to ${INSTALL_DIR}/${BINARY}"
53+
54+
case ":${PATH}:" in
55+
*":${INSTALL_DIR}:"*) ;;
56+
*)
57+
echo ""
58+
echo "NOTE: ${INSTALL_DIR} is not in your PATH."
59+
echo "Add it by running:"
60+
echo " export PATH=\"${INSTALL_DIR}:\$PATH\""
61+
echo "Or add that line to your ~/.zshrc / ~/.bashrc."
62+
;;
63+
esac
64+
5765
"${INSTALL_DIR}/${BINARY}" --version
5866
}
5967

src/api/admin/mod.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,63 @@ const RUNS_STRIP_FIELDS: &[&str] = &[
5555

5656
const PROJECT_STRIP_FIELDS: &[&str] = &["freshness_job", "docs_job", "group_permissions"];
5757

58+
pub const ACCOUNTS_TABLE_FIELDS: &[&str] = &[
59+
"id",
60+
"name",
61+
"state",
62+
"plan",
63+
"developer_seats",
64+
"run_slots",
65+
"created_at",
66+
];
67+
68+
pub const PROJECTS_TABLE_FIELDS: &[&str] = &[
69+
"id",
70+
"name",
71+
"description",
72+
"state",
73+
"repository_id",
74+
"connection_id",
75+
"created_at",
76+
];
77+
78+
pub const JOBS_TABLE_FIELDS: &[&str] = &[
79+
"id",
80+
"name",
81+
"project_id",
82+
"environment_id",
83+
"job_type",
84+
"state",
85+
"dbt_version",
86+
];
87+
88+
pub const RUNS_TABLE_FIELDS: &[&str] = &[
89+
"id",
90+
"job_id",
91+
"project_id",
92+
"status_humanized",
93+
"duration",
94+
"created_at",
95+
"finished_at",
96+
];
97+
98+
pub const ENVIRONMENTS_TABLE_FIELDS: &[&str] = &[
99+
"id",
100+
"name",
101+
"type",
102+
"state",
103+
"project_id",
104+
"dbt_version",
105+
"created_at",
106+
];
107+
108+
pub fn table_view(val: &Value, fields: &[&str]) -> Value {
109+
match val {
110+
Value::Array(arr) => Value::Array(arr.iter().map(|v| keep_fields(v, fields)).collect()),
111+
other => keep_fields(other, fields),
112+
}
113+
}
114+
58115
pub fn compact_job(val: &Value) -> Value {
59116
keep_fields(val, JOBS_COMPACT_FIELDS)
60117
}

src/api/discovery/mod.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ pub fn metadata_url(host: &str) -> String {
4141
}
4242

4343
pub fn require_environment_id(config: &Config) -> Result<u64> {
44-
config.environment_id.ok_or_else(|| {
45-
DbtpError::config(
46-
"environment_id is required for Discovery API; \
47-
set via --environment-id, DBTP_ENVIRONMENT_ID, or `dbtp configure`",
48-
)
49-
})
44+
config
45+
.environment_id_u64()
46+
.ok_or_else(|| {
47+
DbtpError::config(
48+
"environment_id is required for Discovery API; \
49+
set via --environment-id, DBTP_ENVIRONMENT_ID, or `dbtp configure`",
50+
)
51+
})
5052
}
5153

5254
/// Extract nodes from paginated GraphQL edges: `[{node: ...}, ...]` -> `[...]`

src/cli/commands/accounts.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use clap::{Args, Subcommand};
22
use serde_json::Value;
33

4-
use crate::api::admin::accounts;
4+
use crate::api::admin::{self, accounts};
55
use crate::core::config::Config;
66
use crate::core::error::Result;
77
use crate::core::rest_client::RestClient;
@@ -24,8 +24,17 @@ pub enum AccountsCommand {
2424
}
2525

2626
pub async fn exec(args: &AccountsArgs, client: &RestClient, config: &Config) -> Result<Value> {
27+
let is_table = config.output == "table" || config.output.is_empty();
28+
2729
match &args.command {
28-
AccountsCommand::List => accounts::list(&config.host, &config.token).await,
30+
AccountsCommand::List => {
31+
let val = accounts::list(&config.host, &config.token).await?;
32+
Ok(if is_table {
33+
admin::table_view(&val, admin::ACCOUNTS_TABLE_FIELDS)
34+
} else {
35+
val
36+
})
37+
}
2938
AccountsCommand::Show { account_id } => match account_id {
3039
Some(id) => accounts::get_by_id(&config.host, &config.token, *id).await,
3140
None => accounts::get(client).await,

0 commit comments

Comments
 (0)