use connectrpc::client::{ClientConfig, ClientTransport, HttpClient};
use headwaters_proto::connect_gen::headwaters::read::v1::ReadServiceClient;
use crate::types::*;
use crate::{Error, HeadwatersConfig};
#[derive(Clone)]
pub struct HeadwatersClient<T = HttpClient>
where
T: ClientTransport,
{
inner: ReadServiceClient<T>,
}
impl HeadwatersClient<HttpClient> {
pub fn connect(base_url: impl Into<String>) -> Result<Self, Error> {
Self::new(HeadwatersConfig::new(base_url))
}
pub fn new(config: HeadwatersConfig) -> Result<Self, Error> {
let uri: http::Uri = config
.base_url
.parse()
.map_err(|_| Error::InvalidBaseUrl(config.base_url.clone()))?;
if uri.scheme_str() == Some("https") {
return Err(Error::InvalidBaseUrl(format!(
"{}: https is not supported yet (TLS is a planned follow-up)",
config.base_url
)));
}
let mut client_config = ClientConfig::new(uri).with_default_timeout(config.timeout);
if let Some(token) = &config.token {
client_config =
client_config.with_default_header("authorization", format!("Bearer {token}"));
}
Ok(Self {
inner: ReadServiceClient::new(HttpClient::plaintext(), client_config),
})
}
pub fn from_env() -> Result<Self, Error> {
Self::new(HeadwatersConfig::from_env()?)
}
}
impl<T> HeadwatersClient<T>
where
T: ClientTransport,
<T::ResponseBody as http_body::Body>::Error: std::fmt::Display,
{
pub async fn list_namespaces(&self) -> Result<ListNamespacesResponse, Error> {
Ok(self
.inner
.list_namespaces(ListNamespacesRequest::default())
.await?
.into_owned())
}
pub async fn list_jobs(
&self,
namespace: &str,
limit: i32,
offset: i32,
) -> Result<ListJobsResponse, Error> {
Ok(self
.inner
.list_jobs(ListJobsRequest {
namespace: namespace.to_string(),
limit,
offset,
..Default::default()
})
.await?
.into_owned())
}
pub async fn get_job(&self, namespace: &str, name: &str) -> Result<JobDetail, Error> {
Ok(self
.inner
.get_job(GetJobRequest {
namespace: namespace.to_string(),
name: name.to_string(),
..Default::default()
})
.await?
.into_owned())
}
pub async fn get_job_runs(
&self,
namespace: &str,
name: &str,
) -> Result<ListRunsResponse, Error> {
Ok(self
.inner
.get_job_runs(GetJobRunsRequest {
namespace: namespace.to_string(),
name: name.to_string(),
..Default::default()
})
.await?
.into_owned())
}
pub async fn list_datasets(
&self,
namespace: &str,
limit: i32,
offset: i32,
) -> Result<ListDatasetsResponse, Error> {
Ok(self
.inner
.list_datasets(ListDatasetsRequest {
namespace: namespace.to_string(),
limit,
offset,
..Default::default()
})
.await?
.into_owned())
}
pub async fn get_dataset(&self, namespace: &str, name: &str) -> Result<Dataset, Error> {
Ok(self
.inner
.get_dataset(GetDatasetRequest {
namespace: namespace.to_string(),
name: name.to_string(),
..Default::default()
})
.await?
.into_owned())
}
pub async fn list_dataset_versions(
&self,
namespace: &str,
name: &str,
limit: i32,
offset: i32,
) -> Result<ListDatasetVersionsResponse, Error> {
Ok(self
.inner
.list_dataset_versions(ListDatasetVersionsRequest {
namespace: namespace.to_string(),
name: name.to_string(),
limit,
offset,
..Default::default()
})
.await?
.into_owned())
}
pub async fn search(
&self,
q: &str,
limit: i32,
kind: EntityKind,
namespace: &str,
) -> Result<SearchResponse, Error> {
Ok(self
.inner
.search(SearchRequest {
q: q.to_string(),
limit,
r#type: kind.into(),
namespace: namespace.to_string(),
..Default::default()
})
.await?
.into_owned())
}
pub async fn get_lineage(&self, node_id: &str, depth: i32) -> Result<LineageGraph, Error> {
Ok(self
.inner
.get_lineage(GetLineageRequest {
node_id: node_id.to_string(),
depth,
..Default::default()
})
.await?
.into_owned())
}
pub async fn get_column_lineage(&self, node_id: &str) -> Result<LineageGraph, Error> {
Ok(self
.inner
.get_column_lineage(GetColumnLineageRequest {
node_id: node_id.to_string(),
..Default::default()
})
.await?
.into_owned())
}
pub async fn list_events(&self, limit: i32, offset: i32) -> Result<ListEventsResponse, Error> {
Ok(self
.inner
.list_events(ListEventsRequest {
limit,
offset,
..Default::default()
})
.await?
.into_owned())
}
pub async fn get_run_facets(&self, run_id: &str) -> Result<RunFacetsResponse, Error> {
Ok(self
.inner
.get_run_facets(GetRunFacetsRequest {
run_id: run_id.to_string(),
..Default::default()
})
.await?
.into_owned())
}
pub async fn list_tags(&self) -> Result<ListTagsResponse, Error> {
Ok(self
.inner
.list_tags(ListTagsRequest::default())
.await?
.into_owned())
}
pub async fn get_tag_downstream(&self, tag: &str) -> Result<TagPropagation, Error> {
Ok(self
.inner
.get_tag_downstream(GetTagDownstreamRequest {
tag: tag.to_string(),
..Default::default()
})
.await?
.into_owned())
}
pub async fn get_lineage_event_stats(
&self,
period: &str,
limit: i32,
) -> Result<StatsResponse, Error> {
Ok(self
.inner
.get_lineage_event_stats(GetLineageEventStatsRequest {
period: period.to_string(),
limit,
..Default::default()
})
.await?
.into_owned())
}
pub async fn get_asset_stats(
&self,
asset: &str,
period: &str,
limit: i32,
) -> Result<StatsResponse, Error> {
Ok(self
.inner
.get_asset_stats(GetAssetStatsRequest {
asset: asset.to_string(),
period: period.to_string(),
limit,
..Default::default()
})
.await?
.into_owned())
}
}
#[cfg(feature = "cloud-auth")]
mod cloud {
use connectrpc::client::ClientConfig;
use headwaters_proto::connect_gen::headwaters::read::v1::ReadServiceClient;
use olai_http::CloudClient;
use olai_http::connectrpc::CloudTransport;
use super::HeadwatersClient;
use crate::{Error, HeadwatersConfig};
fn cloud_config(config: &HeadwatersConfig) -> Result<ClientConfig, Error> {
let uri: http::Uri = config
.base_url
.parse()
.map_err(|_| Error::InvalidBaseUrl(config.base_url.clone()))?;
Ok(ClientConfig::new(uri).with_default_timeout(config.timeout))
}
impl HeadwatersClient<CloudTransport> {
pub fn new_with_cloud(
config: HeadwatersConfig,
client: CloudClient,
) -> Result<Self, Error> {
let client_config = cloud_config(&config)?;
Ok(Self {
inner: ReadServiceClient::new(CloudTransport::new(client), client_config),
})
}
pub fn new_aws<I, K, V>(config: HeadwatersConfig, options: I) -> Result<Self, Error>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: Into<String>,
{
Self::new_with_cloud(config, CloudClient::new_aws(options, None)?)
}
pub fn new_azure<I, K, V>(config: HeadwatersConfig, options: I) -> Result<Self, Error>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: Into<String>,
{
Self::new_with_cloud(config, CloudClient::new_azure(options, None)?)
}
pub fn new_google<I, K, V>(config: HeadwatersConfig, options: I) -> Result<Self, Error>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: Into<String>,
{
Self::new_with_cloud(config, CloudClient::new_google(options, None)?)
}
pub fn new_databricks<I, K, V>(config: HeadwatersConfig, options: I) -> Result<Self, Error>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: Into<String>,
{
Self::new_with_cloud(config, CloudClient::new_databricks(options, None)?)
}
pub fn from_env() -> Result<Self, Error> {
let config = HeadwatersConfig::from_env()?;
let provider = std::env::var(crate::ENV_AUTH).unwrap_or_default();
let empty: [(&str, &str); 0] = [];
match provider.to_ascii_lowercase().as_str() {
"aws" => Self::new_aws(config, empty),
"azure" => Self::new_azure(config, empty),
"gcp" | "google" => Self::new_google(config, empty),
"databricks" => Self::new_databricks(config, empty),
other => Err(Error::InvalidBaseUrl(format!(
"unknown {}={other:?} (expected aws|azure|gcp|databricks)",
crate::ENV_AUTH
))),
}
}
}
}