Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,21 @@ pub enum Commands {
command: SkillCommands,
},

/// Retrieve a stored query result by ID
/// Retrieve a stored query result by ID, or list recent results
Results {
/// Result ID
result_id: String,
/// Result ID (omit to use a subcommand)
result_id: Option<String>,

/// Workspace ID (defaults to first workspace from login)
#[arg(long)]
#[arg(long, global = true)]
workspace_id: Option<String>,

/// Output format
#[arg(long, default_value = "table", value_parser = ["table", "json", "csv"])]
format: String,

#[command(subcommand)]
command: Option<ResultsCommands>,
},
}

Expand Down Expand Up @@ -400,6 +403,24 @@ pub enum SkillCommands {
Status,
}

#[derive(Subcommand)]
pub enum ResultsCommands {
/// List stored query results
List {
/// Maximum number of results (default: 100, max: 1000)
#[arg(long)]
limit: Option<u32>,

/// Pagination offset
#[arg(long)]
offset: Option<u32>,

/// Output format
#[arg(long, default_value = "table", value_parser = ["table", "json", "yaml"])]
format: String,
},
}

#[derive(Subcommand)]
pub enum TablesCommands {
/// List all tables in a workspace
Expand Down
19 changes: 16 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mod workspace;

use anstyle::AnsiColor;
use clap::{Parser, builder::Styles};
use command::{AuthCommands, Commands, ConnectionsCommands, ConnectionsCreateCommands, DatasetsCommands, SkillCommands, TablesCommands, WorkspaceCommands};
use command::{AuthCommands, Commands, ConnectionsCommands, ConnectionsCreateCommands, DatasetsCommands, ResultsCommands, SkillCommands, TablesCommands, WorkspaceCommands};

#[derive(Parser)]
#[command(name = "hotdata", version, about = concat!("HotData CLI - Command line interface for HotData (v", env!("CARGO_PKG_VERSION"), ")"), long_about = None, disable_version_flag = true)]
Expand Down Expand Up @@ -149,9 +149,22 @@ fn main() {
}
SkillCommands::Status => skill::status(),
},
Commands::Results { result_id, workspace_id, format } => {
Commands::Results { result_id, workspace_id, format, command } => {
let workspace_id = resolve_workspace(workspace_id);
results::get(&result_id, &workspace_id, &format)
match command {
Some(ResultsCommands::List { limit, offset, format }) => {
results::list(&workspace_id, limit, offset, &format)
}
None => {
match result_id {
Some(id) => results::get(&id, &workspace_id, &format),
None => {
eprintln!("error: provide a result ID or use 'results list'");
std::process::exit(1);
}
}
}
}
}
},
}
Expand Down
92 changes: 91 additions & 1 deletion src/results.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::config;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Deserialize)]
Expand All @@ -26,6 +26,96 @@ fn value_to_string(v: &Value) -> String {
}
}

#[derive(Deserialize, Serialize)]
struct ResultEntry {
id: String,
status: String,
created_at: String,
}

#[derive(Deserialize)]
struct ListResponse {
results: Vec<ResultEntry>,
count: u64,
has_more: bool,
}

pub fn list(workspace_id: &str, limit: Option<u32>, offset: Option<u32>, format: &str) {
let profile_config = match config::load("default") {
Ok(c) => c,
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
};

let api_key = match &profile_config.api_key {
Some(key) if key != "PLACEHOLDER" => key.clone(),
_ => {
eprintln!("error: not authenticated. Run 'hotdata auth login' to log in.");
std::process::exit(1);
}
};

let mut url = format!("{}/results", profile_config.api_url);
let mut params = vec![];
if let Some(l) = limit { params.push(format!("limit={l}")); }
if let Some(o) = offset { params.push(format!("offset={o}")); }
if !params.is_empty() { url = format!("{url}?{}", params.join("&")); }

let client = reqwest::blocking::Client::new();
let resp = match client
.get(&url)
.header("Authorization", format!("Bearer {api_key}"))
.header("X-Workspace-Id", workspace_id)
.send()
{
Ok(r) => r,
Err(e) => {
eprintln!("error connecting to API: {e}");
std::process::exit(1);
}
};

if !resp.status().is_success() {
use crossterm::style::Stylize;
eprintln!("{}", crate::util::api_error(resp.text().unwrap_or_default()).red());
std::process::exit(1);
}

let body: ListResponse = match resp.json() {
Ok(v) => v,
Err(e) => {
eprintln!("error parsing response: {e}");
std::process::exit(1);
}
};

match format {
"json" => println!("{}", serde_json::to_string_pretty(&body.results).unwrap()),
"yaml" => print!("{}", serde_yaml::to_string(&body.results).unwrap()),
"table" => {
if body.results.is_empty() {
use crossterm::style::Stylize;
eprintln!("{}", "No results found.".dark_grey());
} else {
let rows: Vec<Vec<String>> = body.results.iter().map(|r| vec![
r.id.clone(),
r.status.clone(),
crate::util::format_date(&r.created_at),
]).collect();
crate::table::print(&["ID", "STATUS", "CREATED AT"], &rows);
}
if body.has_more {
let next = offset.unwrap_or(0) + body.count as u32;
use crossterm::style::Stylize;
eprintln!("{}", format!("showing {} results — use --offset {next} for more", body.count).dark_grey());
}
}
_ => unreachable!(),
}
}

pub fn get(result_id: &str, workspace_id: &str, format: &str) {
let profile_config = match config::load("default") {
Ok(c) => c,
Expand Down
Loading