1use chrono::{DateTime, Utc};
9use infrastore_core::{
10 ForecastSummaryRow, KeyIdentity, OwnerCategory, Period, RequestedType, Result as CoreResult,
11 StaticConsistency, StaticSummaryRow, TimeSeriesCountsDetailed, TimeSeriesData, TimeSeriesError,
12 TimeSeriesKey, TimeSeriesMetadata, TimeSeriesType,
13};
14
15fn iso_to_period(s: &str) -> CoreResult<Period> {
18 Period::from_iso8601(s).map_err(|e| TimeSeriesError::ConnectionError(e.to_string()))
19}
20
21fn opt_iso_to_period(s: Option<String>) -> CoreResult<Option<Period>> {
22 s.filter(|s| !s.is_empty())
23 .map(|s| iso_to_period(&s))
24 .transpose()
25}
26
27fn convert_err(e: impl std::fmt::Display) -> TimeSeriesError {
30 TimeSeriesError::IntegrityError(format!("convert: {e}"))
31}
32
33fn decode_full_key(k: infrastore_proto::pb::TimeSeriesKey) -> CoreResult<TimeSeriesKey> {
35 full_key_from_pb(k).map_err(convert_err)
36}
37
38#[allow(clippy::too_many_arguments)]
41fn build_list_req(
42 owner_id: Option<i64>,
43 owner_category: Option<OwnerCategory>,
44 owner_type: Option<String>,
45 time_series_type: Option<TimeSeriesType>,
46 name: Option<String>,
47 resolution: Option<Period>,
48 interval: Option<Period>,
49 features: Option<&infrastore_core::Features>,
50) -> ListReq {
51 ListReq {
52 owner_id,
53 owner_category: owner_category.map(|c| pb::OwnerCategory::from(c) as i32),
54 owner_type,
55 time_series_type: time_series_type.map(|t| pb::TimeSeriesType::from(t) as i32),
56 name,
57 resolution: resolution.map(|p| p.to_iso8601()),
58 interval: interval.map(|p| p.to_iso8601()),
59 features: features.map(features_to_pb),
60 }
61}
62use infrastore_proto::convert::{
63 features_to_pb, forecast_summary_row_from_pb, full_key_from_pb, get_resp_to_time_series_data,
64 key_to_pb, metadata_from_pb, requested_type_to_pb, static_summary_row_from_pb,
65};
66use infrastore_proto::pb::{
67 self, BulkReadReq, ConsistencyReq, CountsReq, EmptyReq, ForecastParamsReq, GetReq, HasReq,
68 IntervalsReq, KeyReq, KeysReq, ListKeysReq, ListOwnerIdsReq, ListReq, ResolutionsReq,
69 ResolveForecastKeyReq, VerifyReq, catalog_store_client::CatalogStoreClient,
70};
71use tokio::sync::Mutex;
72use tonic::transport::Channel;
73
74pub struct RemoteClient {
78 inner: Mutex<CatalogStoreClient<Channel>>,
79}
80
81impl RemoteClient {
82 pub async fn connect(addr: String) -> CoreResult<Self> {
83 let channel = Channel::from_shared(addr.clone())
84 .map_err(|e| TimeSeriesError::ConnectionError(format!("invalid uri {addr}: {e}")))?
85 .connect()
86 .await
87 .map_err(|e| TimeSeriesError::ConnectionError(format!("{addr}: {e}")))?;
88 Ok(Self::from_channel(channel))
89 }
90
91 pub fn from_channel(channel: Channel) -> Self {
92 Self {
93 inner: Mutex::new(CatalogStoreClient::new(channel)),
94 }
95 }
96
97 fn map_status(s: tonic::Status) -> TimeSeriesError {
98 match s.code() {
99 tonic::Code::NotFound => TimeSeriesError::NotFound,
100 tonic::Code::AlreadyExists => TimeSeriesError::DuplicateTimeSeries,
101 tonic::Code::InvalidArgument => {
102 TimeSeriesError::InvalidParameter(s.message().to_string())
103 }
104 tonic::Code::DataLoss => TimeSeriesError::IntegrityError(s.message().to_string()),
105 tonic::Code::FailedPrecondition => {
106 TimeSeriesError::InvalidParameter(s.message().to_string())
107 }
108 _ => TimeSeriesError::ConnectionError(format!("{}: {}", s.code(), s.message())),
109 }
110 }
111
112 #[allow(clippy::too_many_arguments)]
113 pub async fn list_time_series(
114 &self,
115 owner_id: Option<i64>,
116 owner_category: Option<OwnerCategory>,
117 owner_type: Option<String>,
118 time_series_type: Option<TimeSeriesType>,
119 name: Option<String>,
120 resolution: Option<Period>,
121 interval: Option<Period>,
122 features: Option<&infrastore_core::Features>,
123 ) -> CoreResult<Vec<TimeSeriesMetadata>> {
124 let req = build_list_req(
125 owner_id,
126 owner_category,
127 owner_type,
128 time_series_type,
129 name,
130 resolution,
131 interval,
132 features,
133 );
134 let mut inner = self.inner.lock().await;
135 let resp = inner
136 .list_time_series(req)
137 .await
138 .map_err(Self::map_status)?
139 .into_inner();
140 let mut out = Vec::with_capacity(resp.metadata.len());
141 for m in resp.metadata {
142 out.push(
143 metadata_from_pb(m).map_err(|e| {
144 TimeSeriesError::IntegrityError(format!("metadata convert: {e}"))
145 })?,
146 );
147 }
148 Ok(out)
149 }
150
151 pub async fn get_time_series(
152 &self,
153 key: &KeyIdentity,
154 time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
155 ) -> CoreResult<TimeSeriesData> {
156 let (start, end) = match time_range {
157 Some((s, e)) => (Some(s.to_rfc3339()), Some(e.to_rfc3339())),
158 None => (None, None),
159 };
160 let req = GetReq {
161 key: Some(key_to_pb(key)),
162 start_rfc3339: start,
163 end_rfc3339: end,
164 };
165 let mut inner = self.inner.lock().await;
166 let resp = inner
167 .get_time_series(req)
168 .await
169 .map_err(Self::map_status)?
170 .into_inner();
171 get_resp_to_time_series_data(resp, key.name.clone())
172 .map_err(|e| TimeSeriesError::IntegrityError(format!("get convert: {e}")))
173 }
174
175 pub async fn get_time_series_keys(
176 &self,
177 owner_id: i64,
178 owner_category: OwnerCategory,
179 ) -> CoreResult<Vec<TimeSeriesKey>> {
180 let mut inner = self.inner.lock().await;
181 let resp = inner
182 .get_time_series_keys(KeysReq {
183 owner_id,
184 owner_category: pb::OwnerCategory::from(owner_category) as i32,
185 })
186 .await
187 .map_err(Self::map_status)?
188 .into_inner();
189 resp.keys.into_iter().map(decode_full_key).collect()
190 }
191
192 pub async fn get_resolutions(
193 &self,
194 time_series_type: Option<TimeSeriesType>,
195 ) -> CoreResult<Vec<Period>> {
196 let req = ResolutionsReq {
197 time_series_type: time_series_type.map(|t| pb::TimeSeriesType::from(t) as i32),
198 };
199 let mut inner = self.inner.lock().await;
200 let resp = inner
201 .get_resolutions(req)
202 .await
203 .map_err(Self::map_status)?
204 .into_inner();
205 resp.resolution.iter().map(|s| iso_to_period(s)).collect()
206 }
207
208 pub async fn get_counts(&self) -> CoreResult<infrastore_core::TimeSeriesCounts> {
209 let mut inner = self.inner.lock().await;
210 let resp = inner
211 .get_counts(CountsReq {})
212 .await
213 .map_err(Self::map_status)?
214 .into_inner();
215 Ok(infrastore_core::TimeSeriesCounts {
216 components_with_time_series: resp.components_with_time_series,
217 static_time_series: resp.static_time_series,
218 forecasts: resp.forecasts,
219 })
220 }
221
222 pub async fn get_forecast_parameters(
223 &self,
224 resolution: Option<Period>,
225 interval: Option<Period>,
226 ) -> CoreResult<infrastore_core::ForecastParameters> {
227 let mut inner = self.inner.lock().await;
228 let resp = inner
229 .get_forecast_parameters(ForecastParamsReq {
230 resolution: resolution.map(|p| p.to_iso8601()),
231 interval: interval.map(|p| p.to_iso8601()),
232 })
233 .await
234 .map_err(Self::map_status)?
235 .into_inner();
236 let initial_timestamp = match resp.initial_timestamp_rfc3339 {
237 Some(s) => Some(
238 DateTime::parse_from_rfc3339(&s)
239 .map_err(|e| TimeSeriesError::ConnectionError(e.to_string()))?
240 .with_timezone(&Utc),
241 ),
242 None => None,
243 };
244 Ok(infrastore_core::ForecastParameters {
245 horizon: opt_iso_to_period(resp.horizon)?,
246 interval: opt_iso_to_period(resp.interval)?,
247 count: resp.count.map(|c| c as usize),
248 resolution: opt_iso_to_period(resp.resolution)?,
249 initial_timestamp,
250 })
251 }
252
253 pub async fn has_time_series(&self, key: &KeyIdentity) -> CoreResult<bool> {
254 let mut inner = self.inner.lock().await;
255 let resp = inner
256 .has_time_series(HasReq {
257 key: Some(key_to_pb(key)),
258 })
259 .await
260 .map_err(Self::map_status)?
261 .into_inner();
262 Ok(resp.present)
263 }
264
265 pub async fn verify_integrity(&self) -> CoreResult<infrastore_core::storage::IntegrityReport> {
266 let mut inner = self.inner.lock().await;
267 let resp = inner
268 .verify_integrity(VerifyReq {})
269 .await
270 .map_err(Self::map_status)?
271 .into_inner();
272 Ok(infrastore_core::storage::IntegrityReport {
273 errors: resp.errors,
274 })
275 }
276
277 #[allow(clippy::too_many_arguments)]
282 pub async fn list_keys(
283 &self,
284 owner_id: Option<i64>,
285 owner_category: Option<OwnerCategory>,
286 owner_type: Option<String>,
287 time_series_type: Option<TimeSeriesType>,
288 name: Option<String>,
289 resolution: Option<Period>,
290 interval: Option<Period>,
291 features: Option<&infrastore_core::Features>,
292 with_hash: bool,
293 ) -> CoreResult<Vec<(TimeSeriesKey, Option<[u8; 32]>)>> {
294 let filter = build_list_req(
295 owner_id,
296 owner_category,
297 owner_type,
298 time_series_type,
299 name,
300 resolution,
301 interval,
302 features,
303 );
304 let mut inner = self.inner.lock().await;
305 let resp = inner
306 .list_keys(ListKeysReq {
307 filter: Some(filter),
308 with_hash,
309 })
310 .await
311 .map_err(Self::map_status)?
312 .into_inner();
313 let mut out = Vec::with_capacity(resp.rows.len());
314 for row in resp.rows {
315 let key = decode_full_key(
316 row.key
317 .ok_or_else(|| convert_err("ListKeys row missing key"))?,
318 )?;
319 let hash = match row.data_hash {
320 Some(h) => Some(
321 <[u8; 32]>::try_from(h.as_slice())
322 .map_err(|_| convert_err("data_hash is not 32 bytes"))?,
323 ),
324 None => None,
325 };
326 out.push((key, hash));
327 }
328 Ok(out)
329 }
330
331 pub async fn get_metadata(&self, key: &KeyIdentity) -> CoreResult<TimeSeriesMetadata> {
333 let mut inner = self.inner.lock().await;
334 let resp = inner
335 .get_metadata(KeyReq {
336 key: Some(key_to_pb(key)),
337 })
338 .await
339 .map_err(Self::map_status)?
340 .into_inner();
341 metadata_from_pb(resp).map_err(convert_err)
342 }
343
344 pub async fn bulk_read(
346 &self,
347 keys: &[&KeyIdentity],
348 time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
349 ) -> CoreResult<Vec<TimeSeriesData>> {
350 let (start_rfc3339, end_rfc3339) = match time_range {
351 Some((s, e)) => (Some(s.to_rfc3339()), Some(e.to_rfc3339())),
352 None => (None, None),
353 };
354 let mut inner = self.inner.lock().await;
355 let resp = inner
356 .bulk_read(BulkReadReq {
357 keys: keys.iter().map(|k| key_to_pb(k)).collect(),
358 start_rfc3339,
359 end_rfc3339,
360 })
361 .await
362 .map_err(Self::map_status)?
363 .into_inner();
364 resp.items
365 .into_iter()
366 .map(|item| get_resp_to_time_series_data(item, String::new()).map_err(convert_err))
367 .collect()
368 }
369
370 pub async fn time_series_counts_detailed(&self) -> CoreResult<TimeSeriesCountsDetailed> {
372 let mut inner = self.inner.lock().await;
373 let resp = inner
374 .get_detailed_counts(EmptyReq {})
375 .await
376 .map_err(Self::map_status)?
377 .into_inner();
378 Ok(TimeSeriesCountsDetailed {
379 components_with_time_series: resp.components_with_time_series,
380 supplemental_attributes_with_time_series: resp.supplemental_attributes_with_time_series,
381 static_time_series_count: resp.static_time_series_count,
382 forecast_count: resp.forecast_count,
383 })
384 }
385
386 pub async fn counts_by_type(&self) -> CoreResult<Vec<(TimeSeriesType, i64)>> {
388 let mut inner = self.inner.lock().await;
389 let resp = inner
390 .get_counts_by_type(EmptyReq {})
391 .await
392 .map_err(Self::map_status)?
393 .into_inner();
394 resp.entries
395 .into_iter()
396 .map(|e| {
397 let t = pb::TimeSeriesType::try_from(e.time_series_type)
398 .map(TimeSeriesType::from)
399 .map_err(|_| convert_err("unknown time_series_type"))?;
400 Ok((t, e.count))
401 })
402 .collect()
403 }
404
405 pub async fn list_owner_ids(
407 &self,
408 category: OwnerCategory,
409 time_series_type: Option<TimeSeriesType>,
410 resolution: Option<Period>,
411 ) -> CoreResult<Vec<i64>> {
412 let mut inner = self.inner.lock().await;
413 let resp = inner
414 .list_owner_ids(ListOwnerIdsReq {
415 owner_category: pb::OwnerCategory::from(category) as i32,
416 time_series_type: time_series_type.map(|t| pb::TimeSeriesType::from(t) as i32),
417 resolution: resolution.map(|p| p.to_iso8601()),
418 })
419 .await
420 .map_err(Self::map_status)?
421 .into_inner();
422 Ok(resp.owner_id)
423 }
424
425 pub async fn get_intervals(
427 &self,
428 time_series_type: Option<TimeSeriesType>,
429 ) -> CoreResult<Vec<Period>> {
430 let mut inner = self.inner.lock().await;
431 let resp = inner
432 .get_intervals(IntervalsReq {
433 time_series_type: time_series_type.map(|t| pb::TimeSeriesType::from(t) as i32),
434 })
435 .await
436 .map_err(Self::map_status)?
437 .into_inner();
438 resp.interval.iter().map(|s| iso_to_period(s)).collect()
439 }
440
441 pub async fn static_summary(&self) -> CoreResult<Vec<StaticSummaryRow>> {
443 let mut inner = self.inner.lock().await;
444 let resp = inner
445 .get_static_summary(EmptyReq {})
446 .await
447 .map_err(Self::map_status)?
448 .into_inner();
449 resp.rows
450 .into_iter()
451 .map(|r| static_summary_row_from_pb(r).map_err(convert_err))
452 .collect()
453 }
454
455 pub async fn forecast_summary(&self) -> CoreResult<Vec<ForecastSummaryRow>> {
457 let mut inner = self.inner.lock().await;
458 let resp = inner
459 .get_forecast_summary(EmptyReq {})
460 .await
461 .map_err(Self::map_status)?
462 .into_inner();
463 resp.rows
464 .into_iter()
465 .map(|r| forecast_summary_row_from_pb(r).map_err(convert_err))
466 .collect()
467 }
468
469 pub async fn check_static_consistency(
471 &self,
472 resolution: Option<Period>,
473 ) -> CoreResult<Vec<StaticConsistency>> {
474 let mut inner = self.inner.lock().await;
475 let resp = inner
476 .check_static_consistency(ConsistencyReq {
477 resolution: resolution.map(|p| p.to_iso8601()),
478 })
479 .await
480 .map_err(Self::map_status)?
481 .into_inner();
482 resp.rows
483 .into_iter()
484 .map(|r| {
485 Ok(StaticConsistency {
486 resolution: iso_to_period(&r.resolution)?,
487 initial_timestamp: DateTime::parse_from_rfc3339(&r.initial_timestamp_rfc3339)
488 .map_err(convert_err)?
489 .with_timezone(&Utc),
490 length: r.length as usize,
491 })
492 })
493 .collect()
494 }
495
496 #[allow(clippy::too_many_arguments)]
498 pub async fn resolve_forecast_key(
499 &self,
500 owner_id: i64,
501 owner_category: OwnerCategory,
502 name: &str,
503 resolution: Option<Period>,
504 interval: Option<Period>,
505 features: infrastore_core::Features,
506 requested: RequestedType,
507 ) -> CoreResult<TimeSeriesKey> {
508 let mut inner = self.inner.lock().await;
509 let resp = inner
510 .resolve_forecast_key(ResolveForecastKeyReq {
511 owner_id,
512 owner_category: pb::OwnerCategory::from(owner_category) as i32,
513 name: name.to_string(),
514 resolution: resolution.map(|p| p.to_iso8601()),
515 interval: interval.map(|p| p.to_iso8601()),
516 features: Some(features_to_pb(&features)),
517 requested: Some(requested_type_to_pb(requested)),
518 })
519 .await
520 .map_err(Self::map_status)?
521 .into_inner();
522 decode_full_key(resp.key.ok_or_else(|| convert_err("missing key"))?)
523 }
524}