mirror of https://github.com/buster-so/buster.git
add in buster-cli and start prepping for analytics/bi app
This commit is contained in:
parent
14d801f44a
commit
a6c5dff778
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
Makefile
|
||||||
|
.vscode/
|
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "buster-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.79"
|
||||||
|
clap = { version = "4.4.18", features = ["derive"] }
|
||||||
|
confy = "0.6.0"
|
||||||
|
dialoguer = "0.11.0"
|
||||||
|
dirs = "5.0.1"
|
||||||
|
indicatif = "0.17.8"
|
||||||
|
inquire = "0.7.5"
|
||||||
|
reqwest = { version = "0.12.9", features = ["json"] }
|
||||||
|
rpassword = "7.3.1"
|
||||||
|
serde = { version = "1.0.196", features = ["derive"] }
|
||||||
|
serde_json = "1.0.113"
|
||||||
|
serde_yaml = "0.9.34"
|
||||||
|
thiserror = "2.0.3"
|
||||||
|
tokio = { version = "1.36.0", features = ["full"] }
|
||||||
|
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
|
@ -0,0 +1 @@
|
||||||
|
# buster-cli
|
|
@ -0,0 +1,29 @@
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
semantic_models:
|
||||||
|
- name: the_name_of_the_semantic_model ## Required
|
||||||
|
description: same as always ## Optional
|
||||||
|
model: ref('some_model') ## Required: the database identifier of the table/view/mv that this semantic model relates to.
|
||||||
|
defaults: ## Required TODO: figure out exactly what this is.
|
||||||
|
agg_time_dimension: dimension_name ## Required if the model contains measures
|
||||||
|
entities:
|
||||||
|
- name: <name of the entity> ## Required
|
||||||
|
type: Primary or natural or foreign or unique ## Required
|
||||||
|
description: A description of the field or role the entity takes in this table ## Optional
|
||||||
|
expr: The field that denotes that entity (transaction_id). ## Optional
|
||||||
|
join_type: many-to-one or one-to-one or one-to-many ## Required on foreign entities helps buster understand how to join the entity to the table.
|
||||||
|
measures:
|
||||||
|
- name: <name of the measure>
|
||||||
|
description: "same as always" ## Optional
|
||||||
|
agg: the aggregation type.
|
||||||
|
expr: the field
|
||||||
|
agg_params: "specific aggregation properties such as a percentile" ## Optional
|
||||||
|
agg_time_dimension: The time field. Defaults to the default agg time dimension for the semantic model. ## Optional
|
||||||
|
non_additive_dimension: "Use these configs when you need non-additive dimensions." ## Optional
|
||||||
|
dimensions:
|
||||||
|
- name: <name of the dimension> # Required
|
||||||
|
type: Categorical or Time # Required
|
||||||
|
label: Recommended adding a string that defines the display value in downstream tools. # Optional
|
||||||
|
type_params: Specific type params such as if the time is primary or used as a partition # Required
|
||||||
|
description: Same as always # Optional
|
||||||
|
expr: The column name or expression. If not provided the default is the dimension name # Optional
|
|
@ -0,0 +1,67 @@
|
||||||
|
pub struct DbtModel {
|
||||||
|
pub version: u32,
|
||||||
|
pub models: Vec<Model>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Model {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub docs: Option<Docs>,
|
||||||
|
pub latest_version: Option<String>,
|
||||||
|
pub deprecation_date: Option<String>,
|
||||||
|
pub access: Option<Access>,
|
||||||
|
pub config: Option<std::collections::HashMap<String, String>>,
|
||||||
|
pub constraints: Option<Vec<String>>,
|
||||||
|
pub tests: Option<Vec<String>>,
|
||||||
|
pub columns: Option<Vec<Column>>,
|
||||||
|
pub time_spine: Option<TimeSpine>,
|
||||||
|
pub versions: Option<Vec<Version>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Docs {
|
||||||
|
pub show: Option<bool>,
|
||||||
|
pub node_color: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Access {
|
||||||
|
Private,
|
||||||
|
Protected,
|
||||||
|
Public,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Column {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub meta: Option<std::collections::HashMap<String, String>>,
|
||||||
|
pub quote: Option<bool>,
|
||||||
|
pub constraints: Option<Vec<String>>,
|
||||||
|
pub tests: Option<Vec<String>>,
|
||||||
|
pub tags: Option<Vec<String>>,
|
||||||
|
pub granularity: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TimeSpine {
|
||||||
|
pub standard_granularity_column: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Version {
|
||||||
|
pub v: String,
|
||||||
|
pub defined_in: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub docs: Option<Docs>,
|
||||||
|
pub access: Option<Access>,
|
||||||
|
pub constraints: Option<Vec<String>>,
|
||||||
|
pub config: Option<std::collections::HashMap<String, String>>,
|
||||||
|
pub tests: Option<Vec<String>>,
|
||||||
|
pub columns: Option<Vec<VersionColumn>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VersionColumn {
|
||||||
|
pub include: Option<String>,
|
||||||
|
pub exclude: Option<Vec<String>>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub quote: Option<bool>,
|
||||||
|
pub constraints: Option<Vec<String>>,
|
||||||
|
pub tests: Option<Vec<String>>,
|
||||||
|
pub tags: Option<Vec<String>>,
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
models:
|
||||||
|
- name: <model name>
|
||||||
|
description: <markdown_string>
|
||||||
|
docs:
|
||||||
|
show: true | false
|
||||||
|
node_color: <color_id> # Use name (such as node_color: purple) or hex code with quotes (such as node_color: "#cd7f32")
|
||||||
|
latest_version: <version_identifier>
|
||||||
|
deprecation_date: <YAML_DateTime>
|
||||||
|
access: private | protected | public
|
||||||
|
config:
|
||||||
|
<model_config>: <config_value>
|
||||||
|
constraints:
|
||||||
|
- <constraint>
|
||||||
|
tests:
|
||||||
|
- <test>
|
||||||
|
- ... # declare additional data tests
|
||||||
|
columns:
|
||||||
|
- name: <column_name> # required
|
||||||
|
description: <markdown_string>
|
||||||
|
meta: { <dictionary> }
|
||||||
|
quote: true | false
|
||||||
|
constraints:
|
||||||
|
- <constraint>
|
||||||
|
tests:
|
||||||
|
- <test>
|
||||||
|
- ... # declare additional data tests
|
||||||
|
tags: [<string>]
|
||||||
|
|
||||||
|
# only required in conjunction with time_spine key
|
||||||
|
granularity: <any supported time granularity>
|
||||||
|
|
||||||
|
- name: ... # declare properties of additional columns
|
||||||
|
|
||||||
|
time_spine:
|
||||||
|
standard_granularity_column: <column_name>
|
||||||
|
|
||||||
|
versions:
|
||||||
|
- v: <version_identifier> # required
|
||||||
|
defined_in: <definition_file_name>
|
||||||
|
description: <markdown_string>
|
||||||
|
docs:
|
||||||
|
show: true | false
|
||||||
|
access: private | protected | public
|
||||||
|
constraints:
|
||||||
|
- <constraint>
|
||||||
|
config:
|
||||||
|
<model_config>: <config_value>
|
||||||
|
tests:
|
||||||
|
- <test>
|
||||||
|
- ... # declare additional data tests
|
||||||
|
columns:
|
||||||
|
# include/exclude columns from the top-level model properties
|
||||||
|
- include: <include_value>
|
||||||
|
exclude: <exclude_list>
|
||||||
|
# specify additional columns
|
||||||
|
- name: <column_name> # required
|
||||||
|
quote: true | false
|
||||||
|
constraints:
|
||||||
|
- <constraint>
|
||||||
|
tests:
|
||||||
|
- <test>
|
||||||
|
- ... # declare additional data tests
|
||||||
|
tags: [<string>]
|
||||||
|
|
||||||
|
semantic_models:
|
||||||
|
- name: the_name_of_the_semantic_model ## Required
|
||||||
|
description: same as always ## Optional
|
||||||
|
model: ref('some_model') ## Required
|
||||||
|
defaults: ## Required
|
||||||
|
agg_time_dimension: dimension_name ## Required if the model contains measures
|
||||||
|
entities:
|
||||||
|
- name: <name of the entity> ## Required
|
||||||
|
type: Primary or natural or foreign or unique ## Required
|
||||||
|
description: A description of the field or role the entity takes in this table ## Optional
|
||||||
|
expr: The field that denotes that entity (transaction_id). ## Optional
|
||||||
|
measures:
|
||||||
|
- name: <name of the measure>
|
||||||
|
description: "same as always" ## Optional
|
||||||
|
agg: the aggregation type.
|
||||||
|
expr: the field
|
||||||
|
agg_params: "specific aggregation properties such as a percentile" ## Optional
|
||||||
|
agg_time_dimension: The time field. Defaults to the default agg time dimension for the semantic model. ## Optional
|
||||||
|
non_additive_dimension: "Use these configs when you need non-additive dimensions." ## Optional
|
||||||
|
dimensions:
|
||||||
|
- name: <name of the dimension> # Required
|
||||||
|
type: Categorical or Time # Required
|
||||||
|
label: Recommended adding a string that defines the display value in downstream tools. # Optional
|
||||||
|
type_params: Specific type params such as if the time is primary or used as a partition # Required
|
||||||
|
description: Same as always # Optional
|
||||||
|
expr: The column name or expression. If not provided the default is the dimension name # Optional
|
|
@ -0,0 +1,60 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use inquire::{Password, Text};
|
||||||
|
|
||||||
|
use crate::utils::{
|
||||||
|
credentials::{get_buster_credentials, set_buster_credentials, BusterCredentials},
|
||||||
|
BusterClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn auth() -> Result<()> {
|
||||||
|
// Get the API key from the credentials file.
|
||||||
|
// If the file doesn't exist, set it to the default.
|
||||||
|
let mut buster_creds = match get_buster_credentials().await {
|
||||||
|
Ok(buster_creds) => buster_creds,
|
||||||
|
Err(_) => match set_buster_credentials(BusterCredentials::default()).await {
|
||||||
|
Ok(_) => get_buster_credentials().await?,
|
||||||
|
Err(e) => anyhow::bail!("Failed to set credentials: {}", e),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let url_input = Text::new("Enter the URL of your Buster API")
|
||||||
|
.with_default(&buster_creds.url)
|
||||||
|
.prompt()?;
|
||||||
|
|
||||||
|
// If no URL at all, error
|
||||||
|
if url_input.is_empty() {
|
||||||
|
anyhow::bail!("URL is required");
|
||||||
|
}
|
||||||
|
buster_creds.url = url_input;
|
||||||
|
|
||||||
|
// Obfuscate the API key for display
|
||||||
|
let obfuscated_api_key = if buster_creds.api_key.is_empty() {
|
||||||
|
String::from("None")
|
||||||
|
} else {
|
||||||
|
format!("{}****", &buster_creds.api_key[..4])
|
||||||
|
};
|
||||||
|
|
||||||
|
let api_key_input = Password::new(&format!("Enter your API key [{obfuscated_api_key}]:"))
|
||||||
|
.without_confirmation()
|
||||||
|
.prompt()?;
|
||||||
|
|
||||||
|
if !api_key_input.is_empty() {
|
||||||
|
buster_creds.api_key = api_key_input;
|
||||||
|
} else if buster_creds.api_key.is_empty() {
|
||||||
|
anyhow::bail!("API key is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the API key.
|
||||||
|
let buster_client = BusterClient::new(buster_creds.url.clone(), buster_creds.api_key.clone())?;
|
||||||
|
|
||||||
|
if !buster_client.validate_api_key().await? {
|
||||||
|
anyhow::bail!("Invalid API key");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the credentials.
|
||||||
|
set_buster_credentials(buster_creds).await?;
|
||||||
|
|
||||||
|
println!("Authentication successful!");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
pub async fn deploy(dbt_only: bool, buster_only: bool) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
pub async fn generate() -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
pub async fn import() -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
pub async fn init() -> Result<()> {
|
||||||
|
// check for buster credentials
|
||||||
|
|
||||||
|
// check if existing dbt project
|
||||||
|
|
||||||
|
// if dbt project, check for dbt yml files
|
||||||
|
|
||||||
|
// create buster project
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
mod auth;
|
||||||
|
mod deploy;
|
||||||
|
mod generate;
|
||||||
|
mod import;
|
||||||
|
mod init;
|
||||||
|
|
||||||
|
pub use auth::auth;
|
||||||
|
pub use deploy::deploy;
|
||||||
|
pub use generate::generate;
|
||||||
|
pub use import::import;
|
||||||
|
pub use init::init;
|
|
@ -0,0 +1,12 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum BusterError {
|
||||||
|
#[error("File not found: {path}")]
|
||||||
|
FileNotFound { path: PathBuf },
|
||||||
|
#[error("Failed to parse file: {error}")]
|
||||||
|
ParseError { error: String },
|
||||||
|
#[error("Failed to write file: {path}")]
|
||||||
|
FileWriteError { path: PathBuf, error: String },
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
mod commands;
|
||||||
|
mod error;
|
||||||
|
mod types;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use commands::{auth, deploy, generate, import, init};
|
||||||
|
|
||||||
|
pub const APP_NAME: &str = "buster";
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
#[clap(rename_all = "lowercase")]
|
||||||
|
pub enum Commands {
|
||||||
|
Init,
|
||||||
|
Auth,
|
||||||
|
Generate,
|
||||||
|
Import,
|
||||||
|
Deploy {
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
dbt_only: bool,
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
buster_only: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
pub struct Args {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub cmd: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
// TODO: All commands should check for an update.
|
||||||
|
let result = match args.cmd {
|
||||||
|
Commands::Init => init().await,
|
||||||
|
Commands::Auth => auth().await,
|
||||||
|
Commands::Generate => generate().await,
|
||||||
|
Commands::Import => import().await,
|
||||||
|
Commands::Deploy {
|
||||||
|
dbt_only,
|
||||||
|
buster_only,
|
||||||
|
} => deploy(dbt_only, buster_only).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use reqwest::{
|
||||||
|
header::{HeaderMap, HeaderValue},
|
||||||
|
Client,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub struct BusterClient {
|
||||||
|
client: Client,
|
||||||
|
base_url: String,
|
||||||
|
api_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ValidateApiKeyResponse {
|
||||||
|
pub valid: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ValidateApiKeyRequest {
|
||||||
|
pub api_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BusterClient {
|
||||||
|
pub fn new(base_url: String, api_key: String) -> Result<Self> {
|
||||||
|
let client = Client::builder().build()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
client,
|
||||||
|
base_url,
|
||||||
|
api_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_headers(&self) -> Result<HeaderMap> {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
"Authorization",
|
||||||
|
HeaderValue::from_str(&format!("Bearer {}", self.api_key))?,
|
||||||
|
);
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn validate_api_key(&self) -> Result<bool> {
|
||||||
|
let request = ValidateApiKeyRequest {
|
||||||
|
api_key: self.api_key.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.post(format!("{}/api/v1/api_keys/validate", self.base_url))
|
||||||
|
.json(&request)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if response.status().is_client_error() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Failed to validate API key. This could be due to an invalid URL"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
match response.json::<ValidateApiKeyResponse>().await {
|
||||||
|
Ok(validate_response) => Ok(validate_response.valid),
|
||||||
|
Err(e) => Err(anyhow::anyhow!(
|
||||||
|
"Failed to parse validate API key response: {}",
|
||||||
|
e
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
mod api;
|
||||||
|
|
||||||
|
pub use api::*;
|
|
@ -0,0 +1,75 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use dirs::home_dir;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
use crate::error::BusterError;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct BusterCredentials {
|
||||||
|
pub url: String,
|
||||||
|
pub api_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BusterCredentials {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
url: String::from("https://api.platform.buster.so"),
|
||||||
|
api_key: String::from(""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_buster_credentials() -> Result<BusterCredentials, BusterError> {
|
||||||
|
let mut path = home_dir().unwrap_or_default();
|
||||||
|
path.push(".buster");
|
||||||
|
path.push("credentials.yml");
|
||||||
|
|
||||||
|
let contents = match fs::read_to_string(&path).await {
|
||||||
|
Ok(contents) => contents,
|
||||||
|
Err(_) => return Err(BusterError::FileNotFound { path }),
|
||||||
|
};
|
||||||
|
|
||||||
|
let creds_yaml = match serde_yaml::from_str(&contents) {
|
||||||
|
Ok(creds_yaml) => creds_yaml,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(BusterError::ParseError {
|
||||||
|
error: e.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(creds_yaml)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_buster_credentials(creds: BusterCredentials) -> Result<(), BusterError> {
|
||||||
|
let mut path = home_dir().unwrap_or_default();
|
||||||
|
path.push(".buster");
|
||||||
|
|
||||||
|
// Create .buster directory if it doesn't exist
|
||||||
|
if !path.exists() {
|
||||||
|
fs::create_dir_all(&path).await.map_err(|e| BusterError::FileWriteError {
|
||||||
|
path: path.clone(),
|
||||||
|
error: e.to_string(),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
path.push("credentials.yml");
|
||||||
|
|
||||||
|
let contents = match serde_yaml::to_string(&creds) {
|
||||||
|
Ok(contents) => contents,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(BusterError::ParseError {
|
||||||
|
error: e.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match fs::write(&path, contents).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(BusterError::FileWriteError {
|
||||||
|
path,
|
||||||
|
error: e.to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod credentials;
|
||||||
|
pub mod profiles;
|
|
@ -0,0 +1,30 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use dirs::home_dir;
|
||||||
|
use serde_yaml::Value;
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
pub async fn get_buster_profiles_yml() -> Result<Value> {
|
||||||
|
let mut path = home_dir().unwrap_or_default();
|
||||||
|
path.push(".buster");
|
||||||
|
path.push("profiles.yml");
|
||||||
|
|
||||||
|
if !fs::try_exists(&path).await? {
|
||||||
|
return Err(anyhow::anyhow!("File not found: {}", path.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = fs::read_to_string(path).await?;
|
||||||
|
Ok(serde_yaml::from_str(&contents)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_dbt_profiles_yml() -> Result<Value> {
|
||||||
|
let mut path = home_dir().unwrap_or_default();
|
||||||
|
path.push(".dbt");
|
||||||
|
path.push("profiles.yml");
|
||||||
|
|
||||||
|
if !fs::try_exists(&path).await? {
|
||||||
|
return Err(anyhow::anyhow!("File not found: {}", path.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = fs::read_to_string(path).await?;
|
||||||
|
Ok(serde_yaml::from_str(&contents)?)
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
mod file;
|
||||||
|
mod buster;
|
||||||
|
|
||||||
|
pub use file::*;
|
||||||
|
pub use buster::*;
|
Loading…
Reference in New Issue