1use connectrpc::client::{ClientConfig, ClientTransport, HttpClient};
4use headwaters_proto::connect_gen::headwaters::read::v1::ReadServiceClient;
5
6use crate::types::*;
7use crate::{Error, HeadwatersConfig};
8
9#[derive(Clone)]
39pub struct HeadwatersClient<T = HttpClient>
40where
41 T: ClientTransport,
42{
43 inner: ReadServiceClient<T>,
44}
45
46impl HeadwatersClient<HttpClient> {
47 pub fn connect(base_url: impl Into<String>) -> Result<Self, Error> {
50 Self::new(HeadwatersConfig::new(base_url))
51 }
52
53 pub fn new(config: HeadwatersConfig) -> Result<Self, Error> {
58 let uri: http::Uri = config
59 .base_url
60 .parse()
61 .map_err(|_| Error::InvalidBaseUrl(config.base_url.clone()))?;
62 if uri.scheme_str() == Some("https") {
63 return Err(Error::InvalidBaseUrl(format!(
64 "{}: https is not supported yet (TLS is a planned follow-up)",
65 config.base_url
66 )));
67 }
68
69 let mut client_config = ClientConfig::new(uri).with_default_timeout(config.timeout);
70 if let Some(token) = &config.token {
71 client_config =
72 client_config.with_default_header("authorization", format!("Bearer {token}"));
73 }
74
75 Ok(Self {
76 inner: ReadServiceClient::new(HttpClient::plaintext(), client_config),
77 })
78 }
79
80 pub fn from_env() -> Result<Self, Error> {
83 Self::new(HeadwatersConfig::from_env()?)
84 }
85}
86
87impl<T> HeadwatersClient<T>
88where
89 T: ClientTransport,
90 <T::ResponseBody as http_body::Body>::Error: std::fmt::Display,
91{
92 pub async fn list_namespaces(&self) -> Result<ListNamespacesResponse, Error> {
96 Ok(self
97 .inner
98 .list_namespaces(ListNamespacesRequest::default())
99 .await?
100 .into_owned())
101 }
102
103 pub async fn list_jobs(
107 &self,
108 namespace: &str,
109 limit: i32,
110 offset: i32,
111 ) -> Result<ListJobsResponse, Error> {
112 Ok(self
113 .inner
114 .list_jobs(ListJobsRequest {
115 namespace: namespace.to_string(),
116 limit,
117 offset,
118 ..Default::default()
119 })
120 .await?
121 .into_owned())
122 }
123
124 pub async fn get_job(&self, namespace: &str, name: &str) -> Result<JobDetail, Error> {
126 Ok(self
127 .inner
128 .get_job(GetJobRequest {
129 namespace: namespace.to_string(),
130 name: name.to_string(),
131 ..Default::default()
132 })
133 .await?
134 .into_owned())
135 }
136
137 pub async fn get_job_runs(
139 &self,
140 namespace: &str,
141 name: &str,
142 ) -> Result<ListRunsResponse, Error> {
143 Ok(self
144 .inner
145 .get_job_runs(GetJobRunsRequest {
146 namespace: namespace.to_string(),
147 name: name.to_string(),
148 ..Default::default()
149 })
150 .await?
151 .into_owned())
152 }
153
154 pub async fn list_datasets(
158 &self,
159 namespace: &str,
160 limit: i32,
161 offset: i32,
162 ) -> Result<ListDatasetsResponse, Error> {
163 Ok(self
164 .inner
165 .list_datasets(ListDatasetsRequest {
166 namespace: namespace.to_string(),
167 limit,
168 offset,
169 ..Default::default()
170 })
171 .await?
172 .into_owned())
173 }
174
175 pub async fn get_dataset(&self, namespace: &str, name: &str) -> Result<Dataset, Error> {
177 Ok(self
178 .inner
179 .get_dataset(GetDatasetRequest {
180 namespace: namespace.to_string(),
181 name: name.to_string(),
182 ..Default::default()
183 })
184 .await?
185 .into_owned())
186 }
187
188 pub async fn list_dataset_versions(
190 &self,
191 namespace: &str,
192 name: &str,
193 limit: i32,
194 offset: i32,
195 ) -> Result<ListDatasetVersionsResponse, Error> {
196 Ok(self
197 .inner
198 .list_dataset_versions(ListDatasetVersionsRequest {
199 namespace: namespace.to_string(),
200 name: name.to_string(),
201 limit,
202 offset,
203 ..Default::default()
204 })
205 .await?
206 .into_owned())
207 }
208
209 pub async fn search(
215 &self,
216 q: &str,
217 limit: i32,
218 kind: EntityKind,
219 namespace: &str,
220 ) -> Result<SearchResponse, Error> {
221 Ok(self
222 .inner
223 .search(SearchRequest {
224 q: q.to_string(),
225 limit,
226 r#type: kind.into(),
227 namespace: namespace.to_string(),
228 ..Default::default()
229 })
230 .await?
231 .into_owned())
232 }
233
234 pub async fn get_lineage(&self, node_id: &str, depth: i32) -> Result<LineageGraph, Error> {
237 Ok(self
238 .inner
239 .get_lineage(GetLineageRequest {
240 node_id: node_id.to_string(),
241 depth,
242 ..Default::default()
243 })
244 .await?
245 .into_owned())
246 }
247
248 pub async fn get_column_lineage(&self, node_id: &str) -> Result<LineageGraph, Error> {
251 Ok(self
252 .inner
253 .get_column_lineage(GetColumnLineageRequest {
254 node_id: node_id.to_string(),
255 ..Default::default()
256 })
257 .await?
258 .into_owned())
259 }
260
261 pub async fn list_events(&self, limit: i32, offset: i32) -> Result<ListEventsResponse, Error> {
263 Ok(self
264 .inner
265 .list_events(ListEventsRequest {
266 limit,
267 offset,
268 ..Default::default()
269 })
270 .await?
271 .into_owned())
272 }
273
274 pub async fn get_run_facets(&self, run_id: &str) -> Result<RunFacetsResponse, Error> {
276 Ok(self
277 .inner
278 .get_run_facets(GetRunFacetsRequest {
279 run_id: run_id.to_string(),
280 ..Default::default()
281 })
282 .await?
283 .into_owned())
284 }
285
286 pub async fn list_tags(&self) -> Result<ListTagsResponse, Error> {
290 Ok(self
291 .inner
292 .list_tags(ListTagsRequest::default())
293 .await?
294 .into_owned())
295 }
296
297 pub async fn get_tag_downstream(&self, tag: &str) -> Result<TagPropagation, Error> {
299 Ok(self
300 .inner
301 .get_tag_downstream(GetTagDownstreamRequest {
302 tag: tag.to_string(),
303 ..Default::default()
304 })
305 .await?
306 .into_owned())
307 }
308
309 pub async fn get_lineage_event_stats(
314 &self,
315 period: &str,
316 limit: i32,
317 ) -> Result<StatsResponse, Error> {
318 Ok(self
319 .inner
320 .get_lineage_event_stats(GetLineageEventStatsRequest {
321 period: period.to_string(),
322 limit,
323 ..Default::default()
324 })
325 .await?
326 .into_owned())
327 }
328
329 pub async fn get_asset_stats(
331 &self,
332 asset: &str,
333 period: &str,
334 limit: i32,
335 ) -> Result<StatsResponse, Error> {
336 Ok(self
337 .inner
338 .get_asset_stats(GetAssetStatsRequest {
339 asset: asset.to_string(),
340 period: period.to_string(),
341 limit,
342 ..Default::default()
343 })
344 .await?
345 .into_owned())
346 }
347}
348
349#[cfg(feature = "cloud-auth")]
350mod cloud {
351 use connectrpc::client::ClientConfig;
352 use headwaters_proto::connect_gen::headwaters::read::v1::ReadServiceClient;
353 use olai_http::CloudClient;
354 use olai_http::connectrpc::CloudTransport;
355
356 use super::HeadwatersClient;
357 use crate::{Error, HeadwatersConfig};
358
359 fn cloud_config(config: &HeadwatersConfig) -> Result<ClientConfig, Error> {
363 let uri: http::Uri = config
364 .base_url
365 .parse()
366 .map_err(|_| Error::InvalidBaseUrl(config.base_url.clone()))?;
367 Ok(ClientConfig::new(uri).with_default_timeout(config.timeout))
368 }
369
370 impl HeadwatersClient<CloudTransport> {
371 pub fn new_with_cloud(
384 config: HeadwatersConfig,
385 client: CloudClient,
386 ) -> Result<Self, Error> {
387 let client_config = cloud_config(&config)?;
388 Ok(Self {
389 inner: ReadServiceClient::new(CloudTransport::new(client), client_config),
390 })
391 }
392
393 pub fn new_aws<I, K, V>(config: HeadwatersConfig, options: I) -> Result<Self, Error>
397 where
398 I: IntoIterator<Item = (K, V)>,
399 K: AsRef<str>,
400 V: Into<String>,
401 {
402 Self::new_with_cloud(config, CloudClient::new_aws(options, None)?)
403 }
404
405 pub fn new_azure<I, K, V>(config: HeadwatersConfig, options: I) -> Result<Self, Error>
408 where
409 I: IntoIterator<Item = (K, V)>,
410 K: AsRef<str>,
411 V: Into<String>,
412 {
413 Self::new_with_cloud(config, CloudClient::new_azure(options, None)?)
414 }
415
416 pub fn new_google<I, K, V>(config: HeadwatersConfig, options: I) -> Result<Self, Error>
419 where
420 I: IntoIterator<Item = (K, V)>,
421 K: AsRef<str>,
422 V: Into<String>,
423 {
424 Self::new_with_cloud(config, CloudClient::new_google(options, None)?)
425 }
426
427 pub fn new_databricks<I, K, V>(config: HeadwatersConfig, options: I) -> Result<Self, Error>
431 where
432 I: IntoIterator<Item = (K, V)>,
433 K: AsRef<str>,
434 V: Into<String>,
435 {
436 Self::new_with_cloud(config, CloudClient::new_databricks(options, None)?)
437 }
438
439 pub fn from_env() -> Result<Self, Error> {
450 let config = HeadwatersConfig::from_env()?;
451 let provider = std::env::var(crate::ENV_AUTH).unwrap_or_default();
452 let empty: [(&str, &str); 0] = [];
453 match provider.to_ascii_lowercase().as_str() {
454 "aws" => Self::new_aws(config, empty),
455 "azure" => Self::new_azure(config, empty),
456 "gcp" | "google" => Self::new_google(config, empty),
457 "databricks" => Self::new_databricks(config, empty),
458 other => Err(Error::InvalidBaseUrl(format!(
459 "unknown {}={other:?} (expected aws|azure|gcp|databricks)",
460 crate::ENV_AUTH
461 ))),
462 }
463 }
464 }
465}