Skip to main content

reinfer_client/
lib.rs

1#![deny(clippy::all)]
2mod error;
3pub mod resources;
4pub mod retry;
5
6use chrono::{DateTime, Utc};
7use http::{header::ACCEPT, Method};
8use log::debug;
9use once_cell::sync::Lazy;
10use reqwest::{
11    blocking::{
12        multipart::{Form, Part},
13        Client as HttpClient, Response as HttpResponse,
14    },
15    header::{self, HeaderMap, HeaderValue},
16    IntoUrl, Proxy, Result as ReqwestResult,
17};
18use resources::{
19    attachments::UploadAttachmentResponse,
20    auth::{RefreshUserPermissionsRequest, RefreshUserPermissionsResponse},
21    bucket::{
22        GetKeyedSyncStateIdsRequest, GetKeyedSyncStateIdsResponse, GetKeyedSyncStatesResponse,
23        KeyedSyncState, KeyedSyncStateId,
24    },
25    bucket_statistics::GetBucketStatisticsResponse,
26    comment::{AttachmentReference, CommentTimestampFilter},
27    dataset::{
28        CreateIxpDatasetRequest, CreateIxpDatasetResponse, GetAllModelsInDatasetRequest,
29        GetAllModelsInDatasetRespone, IxpDatasetNew, QueryRequestParams, QueryResponse,
30        StatisticsRequestParams as DatasetStatisticsRequestParams, SummaryRequestParams,
31        SummaryResponse, UploadIxpDocumentResponse, UserModelMetadata,
32    },
33    documents::{Document, SyncRawEmailsRequest, SyncRawEmailsResponse},
34    email::{Email, GetEmailResponse},
35    integration::{
36        GetIntegrationResponse, GetIntegrationsResponse, Integration, NewIntegration,
37        PostIntegrationRequest, PostIntegrationResponse, PutIntegrationRequest,
38        PutIntegrationResponse,
39    },
40    label_def::{CreateOrUpdateLabelDefsBulkRequest, CreateOrUpdateLabelDefsBulkResponse},
41    project::ForceDeleteProject,
42    quota::{GetQuotasResponse, Quota},
43    source::StatisticsRequestParams as SourceStatisticsRequestParams,
44    stream::{GetStreamResponse, NewStream, PutStreamRequest, PutStreamResponse},
45    tenant_id::UiPathTenantId,
46    validation::{
47        LabelValidation, LabelValidationRequest, LabelValidationResponse, ValidationResponse,
48    },
49};
50use serde::{Deserialize, Serialize};
51use serde_json::json;
52use std::{
53    cell::Cell,
54    fmt::{Debug, Display},
55    io::Read,
56    path::{Path, PathBuf},
57    time::Duration,
58};
59use url::Url;
60
61use crate::resources::{
62    audit::{AuditQueryFilter, AuditQueryRequest, AuditQueryResponse},
63    bucket::{
64        CreateRequest as CreateBucketRequest, CreateResponse as CreateBucketResponse,
65        GetAvailableResponse as GetAvailableBucketsResponse, GetResponse as GetBucketResponse,
66    },
67    bucket_statistics::Statistics as BucketStatistics,
68    comment::{
69        GetAnnotationsResponse, GetCommentResponse, GetLabellingsAfter, GetPredictionsResponse,
70        GetRecentRequest, PutCommentsRequest, PutCommentsResponse, RecentCommentsPage,
71        SyncCommentsRequest, UpdateAnnotationsRequest,
72    },
73    dataset::{
74        CreateRequest as CreateDatasetRequest, CreateResponse as CreateDatasetResponse,
75        GetAvailableResponse as GetAvailableDatasetsResponse, GetResponse as GetDatasetResponse,
76        UpdateRequest as UpdateDatasetRequest, UpdateResponse as UpdateDatasetResponse,
77    },
78    email::{PutEmailsRequest, PutEmailsResponse},
79    project::{
80        CreateProjectRequest, CreateProjectResponse, GetProjectResponse, GetProjectsResponse,
81        UpdateProjectRequest, UpdateProjectResponse,
82    },
83    quota::{CreateQuota, TenantQuotaKind},
84    source::{
85        CreateRequest as CreateSourceRequest, CreateResponse as CreateSourceResponse,
86        GetAvailableResponse as GetAvailableSourcesResponse, GetResponse as GetSourceResponse,
87        UpdateRequest as UpdateSourceRequest, UpdateResponse as UpdateSourceResponse,
88    },
89    statistics::GetResponse as GetStatisticsResponse,
90    stream::{
91        AdvanceRequest as StreamAdvanceRequest, FetchRequest as StreamFetchRequest,
92        GetStreamsResponse, ResetRequest as StreamResetRequest,
93        TagExceptionsRequest as TagStreamExceptionsRequest,
94    },
95    tenant_id::TenantId,
96    user::{
97        CreateRequest as CreateUserRequest, CreateResponse as CreateUserResponse,
98        GetAvailableResponse as GetAvailableUsersResponse,
99        GetCurrentResponse as GetCurrentUserResponse, GetResponse as GetUserResponse,
100        PostUserRequest, PostUserResponse, WelcomeEmailResponse,
101    },
102    EmptySuccess, Response,
103};
104
105use crate::retry::{Retrier, RetryConfig};
106
107pub use crate::{
108    error::{Error, Result},
109    resources::{
110        bucket::{
111            Bucket, BucketType, FullName as BucketFullName, Id as BucketId,
112            Identifier as BucketIdentifier, Name as BucketName, NewBucket,
113        },
114        comment::{
115            AnnotatedComment, Comment, CommentFilter, CommentPredictionsThreshold,
116            CommentsIterPage, Continuation, EitherLabelling, Entities, Entity,
117            GetCommentPredictionsRequest, HasAnnotations, Id as CommentId, Label, Labelling,
118            Message, MessageBody, MessageSignature, MessageSubject, NewAnnotatedComment,
119            NewComment, NewEntities, NewLabelling, NewMoonForm, PredictedLabel, Prediction,
120            PropertyMap, PropertyValue, Sentiment, SyncCommentsResponse, TriggerLabelThreshold,
121            Uid as CommentUid,
122        },
123        dataset::{
124            Dataset, FullName as DatasetFullName, Id as DatasetId, Identifier as DatasetIdentifier,
125            ModelVersion, Name as DatasetName, NewDataset, UpdateDataset,
126        },
127        email::{
128            Continuation as EmailContinuation, EmailsIterPage, EmailsQueryPage, Id as EmailId,
129            Mailbox, MimeContent, NewEmail,
130        },
131        entity_def::{EntityDef, Id as EntityDefId, Name as EntityName, NewEntityDef},
132        integration::FullName as IntegrationFullName,
133        label_def::{
134            LabelDef, LabelDefPretrained, MoonFormFieldDef, Name as LabelName, NewLabelDef,
135            NewLabelDefPretrained, PretrainedId as LabelDefPretrainedId,
136        },
137        label_group::{
138            LabelGroup, Name as LabelGroupName, NewLabelGroup, DEFAULT_LABEL_GROUP_NAME,
139        },
140        project::{NewProject, Project, ProjectName, UpdateProject},
141        source::{
142            FullName as SourceFullName, Id as SourceId, Identifier as SourceIdentifier,
143            Name as SourceName, NewSource, Source, SourceKind, TransformTag, UpdateSource,
144        },
145        statistics::Statistics as CommentStatistics,
146        stream::{
147            Batch as StreamBatch, FullName as StreamFullName, SequenceId as StreamSequenceId,
148            Stream, StreamException, StreamExceptionMetadata,
149        },
150        user::{
151            Email as UserEmail, GlobalPermission, Id as UserId, Identifier as UserIdentifier,
152            ModifiedPermissions, NewUser, ProjectPermission, UpdateUser, User, Username,
153        },
154    },
155};
156
157#[derive(Clone, Debug, PartialEq, Eq)]
158pub struct Token(pub String);
159
160pub trait SplittableRequest {
161    fn split(self) -> impl Iterator<Item = Self>
162    where
163        Self: Sized;
164
165    fn count(&self) -> usize;
166}
167
168pub struct SplitableRequestResponse<ResponseT>
169where
170    for<'de> ResponseT: Deserialize<'de> + ReducibleResponse,
171{
172    pub response: ResponseT,
173    pub num_failed: usize,
174}
175
176pub trait ReducibleResponse {
177    fn merge(self, _b: Self) -> Self
178    where
179        Self: std::default::Default,
180    {
181        Default::default()
182    }
183
184    fn empty() -> Self
185    where
186        Self: std::default::Default,
187    {
188        Default::default()
189    }
190}
191
192pub struct Config {
193    pub endpoint: Url,
194    pub token: Token,
195    pub accept_invalid_certificates: bool,
196    pub proxy: Option<Url>,
197    /// Retry settings to use, if any. This will apply to all requests except for POST requests
198    /// which are not idempotent (as they cannot be naively retried).
199    pub retry_config: Option<RetryConfig>,
200}
201
202impl Default for Config {
203    fn default() -> Self {
204        Config {
205            endpoint: DEFAULT_ENDPOINT.clone(),
206            token: Token("".to_owned()),
207            accept_invalid_certificates: false,
208            proxy: None,
209            retry_config: None,
210        }
211    }
212}
213
214#[derive(Debug)]
215pub struct Client {
216    endpoints: Endpoints,
217    http_client: HttpClient,
218    headers: HeaderMap,
219    retrier: Option<Retrier>,
220}
221
222#[derive(Serialize)]
223pub struct GetLabellingsInBulk<'a> {
224    pub source_id: &'a SourceId,
225    pub return_predictions: &'a bool,
226
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub after: &'a Option<GetLabellingsAfter>,
229
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub limit: &'a Option<usize>,
232}
233
234#[derive(Serialize)]
235pub struct GetCommentsIterPageQuery<'a> {
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub from_timestamp: Option<DateTime<Utc>>,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub to_timestamp: Option<DateTime<Utc>>,
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub after: Option<&'a Continuation>,
242    pub limit: usize,
243    pub include_markup: bool,
244}
245
246#[derive(Serialize)]
247pub struct GetEmailsIterPageQuery<'a> {
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub continuation: Option<&'a EmailContinuation>,
250    pub limit: usize,
251}
252
253#[derive(Serialize)]
254pub struct QueryEmailsPageRequest<'a> {
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub continuation: Option<&'a EmailContinuation>,
257    pub limit: usize,
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub from_timestamp: Option<DateTime<Utc>>,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub to_timestamp: Option<DateTime<Utc>>,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub mailbox_name: Option<&'a str>,
264}
265
266#[derive(Serialize)]
267pub struct GetCommentQuery {
268    pub include_markup: bool,
269}
270
271#[derive(Serialize)]
272pub struct GetEmailQuery {
273    pub id: String,
274}
275
276impl Client {
277    /// Create a new API client.
278    pub fn new(config: Config) -> Result<Client> {
279        let http_client = build_http_client(&config)?;
280        let headers = build_headers(&config)?;
281        let endpoints = Endpoints::new(config.endpoint)?;
282        let retrier = config.retry_config.map(Retrier::new);
283        Ok(Client {
284            endpoints,
285            http_client,
286            headers,
287            retrier,
288        })
289    }
290
291    /// Get the base url for the client
292    pub fn base_url(&self) -> &Url {
293        &self.endpoints.base
294    }
295
296    /// List all visible sources.
297    pub fn get_sources(&self) -> Result<Vec<Source>> {
298        Ok(self
299            .get::<_, GetAvailableSourcesResponse>(self.endpoints.sources.clone())?
300            .sources)
301    }
302
303    /// Get a source by either id or name.
304    pub fn get_user(&self, user: impl Into<UserIdentifier>) -> Result<User> {
305        Ok(match user.into() {
306            UserIdentifier::Id(user_id) => {
307                self.get::<_, GetUserResponse>(self.endpoints.user_by_id(&user_id)?)?
308                    .user
309            }
310        })
311    }
312
313    /// Get a source by either id or name.
314    pub fn get_source(&self, source: impl Into<SourceIdentifier>) -> Result<Source> {
315        Ok(match source.into() {
316            SourceIdentifier::Id(source_id) => {
317                self.get::<_, GetSourceResponse>(self.endpoints.source_by_id(&source_id)?)?
318                    .source
319            }
320            SourceIdentifier::FullName(source_name) => {
321                self.get::<_, GetSourceResponse>(self.endpoints.source_by_name(&source_name)?)?
322                    .source
323            }
324        })
325    }
326
327    pub fn create_label_defs_bulk(
328        &self,
329        dataset_name: &DatasetFullName,
330        label_group: LabelGroupName,
331        label_defs: Vec<NewLabelDef>,
332    ) -> Result<()> {
333        self.put::<_, _, CreateOrUpdateLabelDefsBulkResponse>(
334            self.endpoints.label_group(dataset_name, label_group)?,
335            CreateOrUpdateLabelDefsBulkRequest { label_defs },
336        )?;
337        Ok(())
338    }
339
340    /// Create a new source.
341    pub fn create_source(
342        &self,
343        source_name: &SourceFullName,
344        options: NewSource<'_>,
345    ) -> Result<Source> {
346        Ok(self
347            .put::<_, _, CreateSourceResponse>(
348                self.endpoints.source_by_name(source_name)?,
349                CreateSourceRequest { source: options },
350            )?
351            .source)
352    }
353
354    /// Update a source.
355    pub fn update_source(
356        &self,
357        source_name: &SourceFullName,
358        options: UpdateSource<'_>,
359    ) -> Result<Source> {
360        Ok(self
361            .post::<_, _, UpdateSourceResponse>(
362                self.endpoints.source_by_name(source_name)?,
363                UpdateSourceRequest { source: options },
364                Retry::Yes,
365            )?
366            .source)
367    }
368
369    /// Delete a source.
370    pub fn delete_source(&self, source: impl Into<SourceIdentifier>) -> Result<()> {
371        let source_id = match source.into() {
372            SourceIdentifier::Id(source_id) => source_id,
373            source @ SourceIdentifier::FullName(_) => self.get_source(source)?.id,
374        };
375        self.delete(self.endpoints.source_by_id(&source_id)?)
376    }
377
378    /// Set a quota
379    pub fn create_quota(
380        &self,
381        target_tenant_id: &TenantId,
382        tenant_quota_kind: TenantQuotaKind,
383        options: CreateQuota,
384    ) -> Result<()> {
385        self.post(
386            self.endpoints.quota(target_tenant_id, tenant_quota_kind)?,
387            options,
388            Retry::Yes,
389        )
390    }
391
392    /// Get quotas for current tenant
393    pub fn get_quotas(&self, tenant_id: &Option<UiPathTenantId>) -> Result<Vec<Quota>> {
394        Ok(self
395            .get::<_, GetQuotasResponse>(self.endpoints.quotas(tenant_id)?)?
396            .quotas)
397    }
398
399    /// Delete a user.
400    pub fn delete_user(&self, user: impl Into<UserIdentifier>) -> Result<()> {
401        let UserIdentifier::Id(user_id) = user.into();
402        self.delete(self.endpoints.user_by_id(&user_id)?)
403    }
404
405    /// Delete comments by id in a source.
406    pub fn delete_comments(
407        &self,
408        source: impl Into<SourceIdentifier>,
409        comments: &[CommentId],
410    ) -> Result<()> {
411        let source_full_name = match source.into() {
412            source @ SourceIdentifier::Id(_) => self.get_source(source)?.full_name(),
413            SourceIdentifier::FullName(source_full_name) => source_full_name,
414        };
415        self.delete_query(
416            self.endpoints.comments_v1(&source_full_name)?,
417            Some(&id_list_query(comments.iter().map(|uid| &uid.0))),
418        )
419    }
420
421    /// Delete emails by id in a bucket.
422    pub fn delete_emails(
423        &self,
424        bucket: impl Into<BucketIdentifier>,
425        emails: &[EmailId],
426    ) -> Result<()> {
427        let bucket_full_name = match bucket.into() {
428            bucket @ BucketIdentifier::Id(_) => self.get_bucket(bucket)?.full_name(),
429            BucketIdentifier::FullName(bucket_full_name) => bucket_full_name,
430        };
431        self.delete_query(
432            self.endpoints.delete_emails(&bucket_full_name)?,
433            Some(&id_list_query(emails.iter().map(|id| &id.0))),
434        )
435    }
436
437    /// Get a page of comments from a source.
438    pub fn get_comments_iter_page(
439        &self,
440        source_name: &SourceFullName,
441        continuation: Option<&ContinuationKind>,
442        to_timestamp: Option<DateTime<Utc>>,
443        limit: usize,
444    ) -> Result<CommentsIterPage> {
445        // Comments are returned from the API in increasing order of their
446        // `timestamp` field.
447        let (from_timestamp, after) = match continuation {
448            // If we have a timestamp, then this is a request for the first page of
449            // a series of comments with timestamps starting from the given time.
450            Some(ContinuationKind::Timestamp(from_timestamp)) => (Some(*from_timestamp), None),
451            // If we have a continuation, then this is a request for page n+1 of
452            // a series of comments, where the continuation came from page n.
453            Some(ContinuationKind::Continuation(after)) => (None, Some(after)),
454            // Otherwise, this is a request for the first page of a series of comments
455            // with timestamps starting from the beginning of time.
456            None => (None, None),
457        };
458        let query_params = GetCommentsIterPageQuery {
459            from_timestamp,
460            to_timestamp,
461            after,
462            limit,
463            include_markup: true,
464        };
465        self.get_query(self.endpoints.comments(source_name)?, Some(&query_params))
466    }
467
468    /// Iterate through all comments for a given dataset query.
469    pub fn get_dataset_query_iter<'a>(
470        &'a self,
471        dataset_name: &'a DatasetFullName,
472        params: &'a mut QueryRequestParams,
473    ) -> DatasetQueryIter<'a> {
474        DatasetQueryIter::new(self, dataset_name, params)
475    }
476
477    /// Iterate through all comments in a source.
478    pub fn get_comments_iter<'a>(
479        &'a self,
480        source_name: &'a SourceFullName,
481        page_size: Option<usize>,
482        timerange: CommentsIterTimerange,
483    ) -> CommentsIter<'a> {
484        CommentsIter::new(self, source_name, page_size, timerange)
485    }
486
487    pub fn get_keyed_sync_state_ids(
488        &self,
489        bucket_id: &BucketId,
490        request: &GetKeyedSyncStateIdsRequest,
491    ) -> Result<Vec<KeyedSyncStateId>> {
492        Ok(self
493            .post::<_, _, GetKeyedSyncStateIdsResponse>(
494                self.endpoints.query_keyed_sync_state_ids(bucket_id)?,
495                Some(&request),
496                Retry::Yes,
497            )?
498            .keyed_sync_state_ids)
499    }
500
501    pub fn delete_keyed_sync_state(
502        &self,
503        bucket_id: &BucketId,
504        id: &KeyedSyncStateId,
505    ) -> Result<()> {
506        self.delete(self.endpoints.keyed_sync_state(bucket_id, id)?)
507    }
508
509    pub fn get_keyed_sync_states(&self, bucket_id: &BucketId) -> Result<Vec<KeyedSyncState>> {
510        Ok(self
511            .get::<_, GetKeyedSyncStatesResponse>(self.endpoints.keyed_sync_states(bucket_id)?)?
512            .keyed_sync_states)
513    }
514
515    /// Get a single of email from a bucket.
516    pub fn get_email(&self, bucket_name: &BucketFullName, id: EmailId) -> Result<Vec<Email>> {
517        let query_params = GetEmailQuery { id: id.0 };
518        Ok(self
519            .get_query::<_, _, GetEmailResponse>(
520                self.endpoints.get_emails(bucket_name)?,
521                Some(&query_params),
522            )?
523            .emails)
524    }
525
526    /// Get a page of emails from a bucket.
527    pub fn get_emails_iter_page(
528        &self,
529        bucket_name: &BucketFullName,
530        continuation: Option<&EmailContinuation>,
531        limit: usize,
532    ) -> Result<EmailsIterPage> {
533        let query_params = GetEmailsIterPageQuery {
534            continuation,
535            limit,
536        };
537        self.post(
538            self.endpoints.get_emails(bucket_name)?,
539            Some(&query_params),
540            Retry::Yes,
541        )
542    }
543
544    /// Iterate through all comments in a source.
545    pub fn get_emails_iter<'a>(
546        &'a self,
547        bucket_name: &'a BucketFullName,
548        page_size: Option<usize>,
549    ) -> EmailsIter<'a> {
550        EmailsIter::new(self, bucket_name, page_size)
551    }
552
553    /// Get a page of emails from a bucket filtered by mailbox name and/or timerange.
554    pub fn query_emails_iter_page(
555        &self,
556        bucket_name: &BucketFullName,
557        filter: &EmailsQueryFilter,
558        continuation: Option<&EmailContinuation>,
559        limit: usize,
560    ) -> Result<EmailsQueryPage> {
561        let request = QueryEmailsPageRequest {
562            continuation,
563            limit,
564            from_timestamp: filter.from_timestamp,
565            to_timestamp: filter.to_timestamp,
566            mailbox_name: filter.mailbox_name.as_deref(),
567        };
568        self.post(
569            self.endpoints.query_emails(bucket_name)?,
570            Some(&request),
571            Retry::Yes,
572        )
573    }
574
575    /// Iterate through emails in a bucket filtered by mailbox name and/or timerange.
576    pub fn query_emails_iter<'a>(
577        &'a self,
578        bucket_name: &'a BucketFullName,
579        filter: EmailsQueryFilter,
580        page_size: Option<usize>,
581    ) -> EmailsQueryIter<'a> {
582        EmailsQueryIter::new(self, bucket_name, filter, page_size)
583    }
584
585    /// Get a single comment by id.
586    pub fn get_comment<'a>(
587        &'a self,
588        source_name: &'a SourceFullName,
589        comment_id: &'a CommentId,
590    ) -> Result<Comment> {
591        let query_params = GetCommentQuery {
592            include_markup: true,
593        };
594        Ok(self
595            .get_query::<_, _, GetCommentResponse>(
596                self.endpoints.comment_by_id(source_name, comment_id)?,
597                Some(&query_params),
598            )?
599            .comment)
600    }
601    pub fn post_integration(
602        &self,
603        name: &IntegrationFullName,
604        integration: &NewIntegration,
605    ) -> Result<PostIntegrationResponse> {
606        self.request(
607            &Method::POST,
608            &self.endpoints.integration(name)?,
609            &Some(PostIntegrationRequest {
610                integration: integration.clone(),
611            }),
612            &None::<()>,
613            &Retry::No,
614        )
615    }
616
617    pub fn put_integration(
618        &self,
619        name: &IntegrationFullName,
620        integration: &NewIntegration,
621    ) -> Result<PutIntegrationResponse> {
622        self.request(
623            &Method::PUT,
624            &self.endpoints.integration(name)?,
625            &Some(PutIntegrationRequest {
626                integration: integration.clone(),
627            }),
628            &None::<()>,
629            &Retry::No,
630        )
631    }
632
633    pub fn put_comments_split_on_failure(
634        &self,
635        source_name: &SourceFullName,
636        comments: Vec<NewComment>,
637        no_charge: bool,
638    ) -> SplitableRequestResponse<PutCommentsResponse> {
639        // Retrying here despite the potential for 409's in order to increase reliability when
640        // working with poor connection
641
642        self.splitable_request(
643            Method::PUT,
644            self.endpoints
645                .put_comments(source_name)
646                .expect("Could not get put_comments endpoint"),
647            PutCommentsRequest { comments },
648            Some(NoChargeQuery { no_charge }),
649            Retry::Yes,
650        )
651    }
652
653    pub fn put_comments(
654        &self,
655        source_name: &SourceFullName,
656        comments: Vec<NewComment>,
657        no_charge: bool,
658    ) -> Result<PutCommentsResponse> {
659        // Retrying here despite the potential for 409's in order to increase reliability when
660        // working with poor connection
661        self.request(
662            &Method::PUT,
663            &self.endpoints.put_comments(source_name)?,
664            &Some(PutCommentsRequest { comments }),
665            &Some(NoChargeQuery { no_charge }),
666            &Retry::Yes,
667        )
668    }
669
670    pub fn put_stream(
671        &self,
672        dataset_name: &DatasetFullName,
673        stream: &NewStream,
674    ) -> Result<PutStreamResponse> {
675        self.put(
676            self.endpoints.streams(dataset_name)?,
677            Some(PutStreamRequest { stream }),
678        )
679    }
680
681    pub fn get_audit_events(
682        &self,
683        minimum_timestamp: Option<DateTime<Utc>>,
684        maximum_timestamp: Option<DateTime<Utc>>,
685        continuation: Option<Continuation>,
686    ) -> Result<AuditQueryResponse> {
687        self.post::<_, _, AuditQueryResponse>(
688            self.endpoints.audit_events_query()?,
689            AuditQueryRequest {
690                continuation,
691                filter: AuditQueryFilter {
692                    timestamp: CommentTimestampFilter {
693                        minimum: minimum_timestamp,
694                        maximum: maximum_timestamp,
695                    },
696                },
697            },
698            Retry::Yes,
699        )
700    }
701    pub fn get_latest_validation(
702        &self,
703        dataset_name: &DatasetFullName,
704    ) -> Result<ValidationResponse> {
705        self.get::<_, ValidationResponse>(self.endpoints.latest_validation(dataset_name)?)
706    }
707
708    pub fn get_validation(
709        &self,
710        dataset_name: &DatasetFullName,
711        model_version: &ModelVersion,
712    ) -> Result<ValidationResponse> {
713        self.get::<_, ValidationResponse>(self.endpoints.validation(dataset_name, model_version)?)
714    }
715
716    pub fn get_labellers(&self, dataset_name: &DatasetFullName) -> Result<Vec<UserModelMetadata>> {
717        Ok(self
718            .post::<_, _, GetAllModelsInDatasetRespone>(
719                self.endpoints.labellers(dataset_name)?,
720                GetAllModelsInDatasetRequest {},
721                Retry::Yes,
722            )?
723            .labellers)
724    }
725
726    pub fn get_label_validation(
727        &self,
728        label: &LabelName,
729        dataset_name: &DatasetFullName,
730        model_version: &ModelVersion,
731    ) -> Result<LabelValidation> {
732        Ok(self
733            .post::<_, _, LabelValidationResponse>(
734                self.endpoints
735                    .label_validation(dataset_name, model_version)?,
736                LabelValidationRequest {
737                    label: label.clone(),
738                },
739                Retry::Yes,
740            )?
741            .label_validation)
742    }
743
744    pub fn sync_comments(
745        &self,
746        source_name: &SourceFullName,
747        comments: Vec<NewComment>,
748        no_charge: bool,
749    ) -> Result<SyncCommentsResponse> {
750        self.request(
751            &Method::POST,
752            &self.endpoints.sync_comments(source_name)?,
753            &Some(SyncCommentsRequest { comments }),
754            &Some(NoChargeQuery { no_charge }),
755            &Retry::Yes,
756        )
757    }
758
759    pub fn sync_comments_split_on_failure(
760        &self,
761        source_name: &SourceFullName,
762        comments: Vec<NewComment>,
763        no_charge: bool,
764    ) -> SplitableRequestResponse<SyncCommentsResponse> {
765        self.splitable_request(
766            Method::POST,
767            self.endpoints
768                .sync_comments(source_name)
769                .expect("Could not get sync_comments endpoint"),
770            SyncCommentsRequest { comments },
771            Some(NoChargeQuery { no_charge }),
772            Retry::Yes,
773        )
774    }
775
776    pub fn sync_raw_emails(
777        &self,
778        source_name: &SourceFullName,
779        documents: &[Document],
780        transform_tag: &TransformTag,
781        include_comments: bool,
782        no_charge: bool,
783    ) -> Result<SyncRawEmailsResponse> {
784        self.request(
785            &Method::POST,
786            &self.endpoints.sync_comments_raw_emails(source_name)?,
787            &Some(SyncRawEmailsRequest {
788                documents,
789                transform_tag,
790                include_comments,
791            }),
792            &Some(NoChargeQuery { no_charge }),
793            &Retry::Yes,
794        )
795    }
796
797    pub fn put_emails_split_on_failure(
798        &self,
799        bucket_name: &BucketFullName,
800        emails: Vec<NewEmail>,
801        no_charge: bool,
802    ) -> SplitableRequestResponse<PutEmailsResponse> {
803        self.splitable_request(
804            Method::PUT,
805            self.endpoints
806                .put_emails(bucket_name)
807                .expect("Could not get put_emails endpoint"),
808            PutEmailsRequest { emails },
809            Some(NoChargeQuery { no_charge }),
810            Retry::Yes,
811        )
812    }
813
814    pub fn put_emails(
815        &self,
816        bucket_name: &BucketFullName,
817        emails: Vec<NewEmail>,
818        no_charge: bool,
819    ) -> Result<PutEmailsResponse> {
820        self.request(
821            &Method::PUT,
822            &self.endpoints.put_emails(bucket_name)?,
823            &Some(PutEmailsRequest { emails }),
824            &Some(NoChargeQuery { no_charge }),
825            &Retry::Yes,
826        )
827    }
828
829    pub fn post_user(&self, user_id: &UserId, user: UpdateUser) -> Result<PostUserResponse> {
830        self.post(
831            self.endpoints.post_user(user_id)?,
832            PostUserRequest { user: &user },
833            Retry::Yes,
834        )
835    }
836
837    pub fn put_comment_audio(
838        &self,
839        source_id: &SourceId,
840        comment_id: &CommentId,
841        audio_path: impl AsRef<Path>,
842    ) -> Result<()> {
843        let form = Form::new()
844            .file("file", audio_path)
845            .map_err(|source| Error::Unknown {
846                message: "PUT comment audio operation failed".to_owned(),
847                source: source.into(),
848            })?;
849        let http_response = self
850            .http_client
851            .put(self.endpoints.comment_audio(source_id, comment_id)?)
852            .headers(self.headers.clone())
853            .multipart(form)
854            .send()
855            .map_err(|source| Error::ReqwestError {
856                message: "PUT comment audio operation failed".to_owned(),
857                source,
858            })?;
859        let status = http_response.status();
860        http_response
861            .json::<Response<EmptySuccess>>()
862            .map_err(Error::BadJsonResponse)?
863            .into_result(status)?;
864        Ok(())
865    }
866
867    pub fn upload_ixp_document(
868        &self,
869        source_id: &SourceId,
870        filename: String,
871        bytes: Vec<u8>,
872    ) -> Result<CommentId> {
873        let endpoint = self.endpoints.ixp_documents(source_id)?;
874
875        let do_request = || {
876            let form = Form::new().part(
877                "file",
878                Part::bytes(bytes.clone()).file_name(filename.clone()),
879            );
880            let request = self
881                .http_client
882                .request(Method::PUT, endpoint.clone())
883                .multipart(form)
884                .headers(self.headers.clone());
885
886            request.send()
887        };
888
889        let result = self.with_retries(do_request);
890
891        let http_response = result.map_err(|source| Error::ReqwestError {
892            source,
893            message: "Operation failed.".to_string(),
894        })?;
895
896        let status = http_response.status();
897
898        Ok(http_response
899            .json::<Response<UploadIxpDocumentResponse>>()
900            .map_err(Error::BadJsonResponse)?
901            .into_result(status)?
902            .comment_id)
903    }
904
905    pub fn upload_comment_attachment(
906        &self,
907        source_id: &SourceId,
908        comment_id: &CommentId,
909        attachment_index: usize,
910        attachment: &PathBuf,
911    ) -> Result<UploadAttachmentResponse> {
912        let url = self
913            .endpoints
914            .attachment_upload(source_id, comment_id, attachment_index)?;
915
916        if !attachment.is_file() || !attachment.exists() {
917            return Err(Error::FileDoesNotExist {
918                path: attachment.clone(),
919            });
920        }
921
922        let do_request = || {
923            let form = Form::new()
924                .file("file", attachment)
925                .map_err(|source| Error::Unknown {
926                    message: "PUT comment attachment operation failed".to_owned(),
927                    source: source.into(),
928                })
929                .unwrap();
930            let request = self
931                .http_client
932                .request(Method::PUT, url.clone())
933                .multipart(form)
934                .headers(self.headers.clone());
935
936            request.send()
937        };
938
939        let result = self.with_retries(do_request);
940
941        let http_response = result.map_err(|source| Error::ReqwestError {
942            source,
943            message: "Operation failed.".to_string(),
944        })?;
945
946        let status = http_response.status();
947
948        http_response
949            .json::<Response<UploadAttachmentResponse>>()
950            .map_err(Error::BadJsonResponse)?
951            .into_result(status)
952    }
953
954    pub fn get_ixp_document(
955        &self,
956        source_id: &SourceId,
957        comment_id: &CommentId,
958    ) -> Result<Vec<u8>> {
959        self.get_octet_stream(&self.endpoints.ixp_document(source_id, comment_id)?)
960    }
961
962    fn get_octet_stream(&self, endpoint: &Url) -> Result<Vec<u8>> {
963        let mut response = self.raw_request(
964            &Method::GET,
965            endpoint,
966            &None::<()>,
967            &None::<()>,
968            &Retry::Yes,
969            None,
970        )?;
971
972        let status = response.status();
973
974        if !status.is_success() {
975            return Err(Error::Api {
976                status_code: status,
977                message: "Bad status code when getting octet stream".to_string(),
978            });
979        }
980
981        let mut buffer = Vec::new();
982
983        response
984            .read_to_end(&mut buffer)
985            .map_err(|source| Error::Unknown {
986                message: "Failed to read buffer".to_string(),
987                source: Box::new(source),
988            })?;
989        Ok(buffer)
990    }
991
992    pub fn get_attachment(&self, reference: &AttachmentReference) -> Result<Vec<u8>> {
993        self.get_octet_stream(&self.endpoints.attachment_reference(reference)?)
994    }
995
996    pub fn get_integrations(&self) -> Result<Vec<Integration>> {
997        Ok(self
998            .get::<_, GetIntegrationsResponse>(self.endpoints.integrations()?)?
999            .integrations)
1000    }
1001
1002    pub fn get_integration(&self, name: &IntegrationFullName) -> Result<Integration> {
1003        Ok(self
1004            .get::<_, GetIntegrationResponse>(self.endpoints.integration(name)?)?
1005            .integration)
1006    }
1007
1008    pub fn get_datasets(&self) -> Result<Vec<Dataset>> {
1009        Ok(self
1010            .get::<_, GetAvailableDatasetsResponse>(self.endpoints.datasets.clone())?
1011            .datasets)
1012    }
1013
1014    pub fn get_dataset<IdentifierT>(&self, dataset: IdentifierT) -> Result<Dataset>
1015    where
1016        IdentifierT: Into<DatasetIdentifier>,
1017    {
1018        Ok(match dataset.into() {
1019            DatasetIdentifier::Id(dataset_id) => {
1020                self.get::<_, GetDatasetResponse>(self.endpoints.dataset_by_id(&dataset_id)?)?
1021                    .dataset
1022            }
1023            DatasetIdentifier::FullName(dataset_name) => {
1024                self.get::<_, GetDatasetResponse>(self.endpoints.dataset_by_name(&dataset_name)?)?
1025                    .dataset
1026            }
1027        })
1028    }
1029
1030    /// Create a ixp dataset.
1031    pub fn create_ixp_dataset(&self, dataset: IxpDatasetNew) -> Result<Dataset> {
1032        Ok(self
1033            .put::<_, _, CreateIxpDatasetResponse>(
1034                self.endpoints.ixp_datasets()?,
1035                CreateIxpDatasetRequest { dataset },
1036            )?
1037            .dataset)
1038    }
1039
1040    /// Create a dataset.
1041    pub fn create_dataset(
1042        &self,
1043        dataset_name: &DatasetFullName,
1044        options: NewDataset<'_>,
1045    ) -> Result<Dataset> {
1046        Ok(self
1047            .put::<_, _, CreateDatasetResponse>(
1048                self.endpoints.dataset_by_name(dataset_name)?,
1049                CreateDatasetRequest { dataset: options },
1050            )?
1051            .dataset)
1052    }
1053
1054    /// Update a dataset.
1055    pub fn update_dataset(
1056        &self,
1057        dataset_name: &DatasetFullName,
1058        options: UpdateDataset<'_>,
1059    ) -> Result<Dataset> {
1060        Ok(self
1061            .post::<_, _, UpdateDatasetResponse>(
1062                self.endpoints.dataset_by_name(dataset_name)?,
1063                UpdateDatasetRequest { dataset: options },
1064                Retry::Yes,
1065            )?
1066            .dataset)
1067    }
1068
1069    pub fn delete_dataset<IdentifierT>(&self, dataset: IdentifierT) -> Result<()>
1070    where
1071        IdentifierT: Into<DatasetIdentifier>,
1072    {
1073        let dataset_id = match dataset.into() {
1074            DatasetIdentifier::Id(dataset_id) => dataset_id,
1075            dataset @ DatasetIdentifier::FullName(_) => self.get_dataset(dataset)?.id,
1076        };
1077        self.delete(self.endpoints.dataset_by_id(&dataset_id)?)
1078    }
1079
1080    /// Get labellings for a given a dataset and a list of comment UIDs.
1081    pub fn get_labellings<'a>(
1082        &self,
1083        dataset_name: &DatasetFullName,
1084        comment_uids: impl Iterator<Item = &'a CommentUid>,
1085    ) -> Result<Vec<AnnotatedComment>> {
1086        Ok(self
1087            .get_query::<_, _, GetAnnotationsResponse>(
1088                self.endpoints.get_labellings(dataset_name)?,
1089                Some(&id_list_query(comment_uids.into_iter().map(|id| &id.0))),
1090            )?
1091            .results)
1092    }
1093
1094    /// Iterate through all reviewed comments in a source.
1095    pub fn get_labellings_iter<'a>(
1096        &'a self,
1097        dataset_name: &'a DatasetFullName,
1098        source_id: &'a SourceId,
1099        return_predictions: bool,
1100        limit: Option<usize>,
1101    ) -> LabellingsIter<'a> {
1102        LabellingsIter::new(self, dataset_name, source_id, return_predictions, limit)
1103    }
1104
1105    /// Get reviewed comments in bulk
1106    pub fn get_labellings_in_bulk(
1107        &self,
1108        dataset_name: &DatasetFullName,
1109        query_parameters: GetLabellingsInBulk<'_>,
1110    ) -> Result<GetAnnotationsResponse> {
1111        self.get_query::<_, _, GetAnnotationsResponse>(
1112            self.endpoints.get_labellings(dataset_name)?,
1113            Some(&query_parameters),
1114        )
1115    }
1116
1117    /// Update labellings for a given a dataset and comment UID.
1118    pub fn update_labelling(
1119        &self,
1120        dataset_name: &DatasetFullName,
1121        comment_uid: &CommentUid,
1122        labelling: Option<&[NewLabelling]>,
1123        entities: Option<&NewEntities>,
1124        moon_forms: Option<&[NewMoonForm]>,
1125    ) -> Result<AnnotatedComment> {
1126        self.post::<_, _, AnnotatedComment>(
1127            self.endpoints.post_labelling(dataset_name, comment_uid)?,
1128            UpdateAnnotationsRequest {
1129                labelling,
1130                entities,
1131                moon_forms,
1132            },
1133            Retry::Yes,
1134        )
1135    }
1136
1137    /// Get predictions for a given a dataset, a model version, and a list of comment UIDs.
1138    pub fn get_comment_predictions<'a>(
1139        &self,
1140        dataset_name: &DatasetFullName,
1141        model_version: &ModelVersion,
1142        comment_uids: impl Iterator<Item = &'a CommentUid>,
1143        threshold: Option<CommentPredictionsThreshold>,
1144        labels: Option<Vec<TriggerLabelThreshold>>,
1145    ) -> Result<Vec<Prediction>> {
1146        Ok(self
1147            .post::<_, _, GetPredictionsResponse>(
1148                self.endpoints
1149                    .get_comment_predictions(dataset_name, model_version)?,
1150                GetCommentPredictionsRequest {
1151                    uids: comment_uids
1152                        .into_iter()
1153                        .map(|id| id.0.clone())
1154                        .collect::<Vec<_>>(),
1155
1156                    threshold,
1157                    labels,
1158                },
1159                Retry::Yes,
1160            )?
1161            .predictions)
1162    }
1163
1164    pub fn get_streams(&self, dataset_name: &DatasetFullName) -> Result<Vec<Stream>> {
1165        Ok(self
1166            .get::<_, GetStreamsResponse>(self.endpoints.streams(dataset_name)?)?
1167            .streams)
1168    }
1169
1170    pub fn get_recent_comments(
1171        &self,
1172        dataset_name: &DatasetFullName,
1173        filter: &CommentFilter,
1174        limit: usize,
1175        continuation: Option<&Continuation>,
1176    ) -> Result<RecentCommentsPage> {
1177        self.post::<_, _, RecentCommentsPage>(
1178            self.endpoints.recent_comments(dataset_name)?,
1179            GetRecentRequest {
1180                limit,
1181                filter,
1182                continuation,
1183            },
1184            Retry::No,
1185        )
1186    }
1187
1188    pub fn refresh_user_permissions(&self) -> Result<RefreshUserPermissionsResponse> {
1189        self.post::<_, _, RefreshUserPermissionsResponse>(
1190            self.endpoints.refresh_user_permissions()?,
1191            RefreshUserPermissionsRequest {},
1192            Retry::Yes,
1193        )
1194    }
1195
1196    pub fn get_current_user(&self) -> Result<User> {
1197        Ok(self
1198            .get::<_, GetCurrentUserResponse>(self.endpoints.current_user.clone())?
1199            .user)
1200    }
1201
1202    pub fn get_users(&self) -> Result<Vec<User>> {
1203        Ok(self
1204            .get::<_, GetAvailableUsersResponse>(self.endpoints.users.clone())?
1205            .users)
1206    }
1207
1208    pub fn create_user(&self, user: NewUser<'_>) -> Result<User> {
1209        Ok(self
1210            .put::<_, _, CreateUserResponse>(
1211                self.endpoints.users.clone(),
1212                CreateUserRequest { user },
1213            )?
1214            .user)
1215    }
1216
1217    pub fn dataset_summary(
1218        &self,
1219        dataset_name: &DatasetFullName,
1220        params: &SummaryRequestParams,
1221    ) -> Result<SummaryResponse> {
1222        self.post::<_, _, SummaryResponse>(
1223            self.endpoints.dataset_summary(dataset_name)?,
1224            serde_json::to_value(params).expect("summary params serialization error"),
1225            Retry::Yes,
1226        )
1227    }
1228
1229    pub fn query_dataset_csv(
1230        &self,
1231        dataset_name: &DatasetFullName,
1232        params: &QueryRequestParams,
1233    ) -> Result<String> {
1234        let response = self
1235            .raw_request(
1236                &Method::POST,
1237                &self.endpoints.query_dataset(dataset_name)?,
1238                &Some(serde_json::to_value(params).expect("query params serialization error")),
1239                &None::<()>,
1240                &Retry::Yes,
1241                Some(HeaderValue::from_str("text/csv").expect("Could not parse csv header")),
1242            )?
1243            .text()
1244            .expect("Could not get csv text");
1245
1246        Ok(response)
1247    }
1248
1249    pub fn query_dataset(
1250        &self,
1251        dataset_name: &DatasetFullName,
1252        params: &QueryRequestParams,
1253    ) -> Result<QueryResponse> {
1254        self.post::<_, _, QueryResponse>(
1255            self.endpoints.query_dataset(dataset_name)?,
1256            serde_json::to_value(params).expect("query params serialization error"),
1257            Retry::Yes,
1258        )
1259    }
1260
1261    pub fn send_welcome_email(&self, user_id: UserId) -> Result<()> {
1262        self.post::<_, _, WelcomeEmailResponse>(
1263            self.endpoints.welcome_email(&user_id)?,
1264            json!({}),
1265            Retry::No,
1266        )?;
1267        Ok(())
1268    }
1269
1270    pub fn get_bucket_statistics(&self, bucket_name: &BucketFullName) -> Result<BucketStatistics> {
1271        Ok(self
1272            .get::<_, GetBucketStatisticsResponse>(self.endpoints.bucket_statistics(bucket_name)?)?
1273            .statistics)
1274    }
1275
1276    pub fn get_dataset_statistics(
1277        &self,
1278        dataset_name: &DatasetFullName,
1279        params: &DatasetStatisticsRequestParams,
1280    ) -> Result<CommentStatistics> {
1281        Ok(self
1282            .post::<_, _, GetStatisticsResponse>(
1283                self.endpoints.dataset_statistics(dataset_name)?,
1284                serde_json::to_value(params)
1285                    .expect("dataset statistics params serialization error"),
1286                Retry::No,
1287            )?
1288            .statistics)
1289    }
1290
1291    pub fn get_source_statistics(
1292        &self,
1293        source_name: &SourceFullName,
1294        params: &SourceStatisticsRequestParams,
1295    ) -> Result<CommentStatistics> {
1296        Ok(self
1297            .post::<_, _, GetStatisticsResponse>(
1298                self.endpoints.source_statistics(source_name)?,
1299                serde_json::to_value(params).expect("source statistics params serialization error"),
1300                Retry::No,
1301            )?
1302            .statistics)
1303    }
1304
1305    /// Create a new bucket.
1306    pub fn create_bucket(
1307        &self,
1308        bucket_name: &BucketFullName,
1309        options: NewBucket<'_>,
1310    ) -> Result<Bucket> {
1311        Ok(self
1312            .put::<_, _, CreateBucketResponse>(
1313                self.endpoints.bucket_by_name(bucket_name)?,
1314                CreateBucketRequest { bucket: options },
1315            )?
1316            .bucket)
1317    }
1318
1319    pub fn get_buckets(&self) -> Result<Vec<Bucket>> {
1320        Ok(self
1321            .get::<_, GetAvailableBucketsResponse>(self.endpoints.buckets.clone())?
1322            .buckets)
1323    }
1324
1325    pub fn get_bucket<IdentifierT>(&self, bucket: IdentifierT) -> Result<Bucket>
1326    where
1327        IdentifierT: Into<BucketIdentifier>,
1328    {
1329        Ok(match bucket.into() {
1330            BucketIdentifier::Id(bucket_id) => {
1331                self.get::<_, GetBucketResponse>(self.endpoints.bucket_by_id(&bucket_id)?)?
1332                    .bucket
1333            }
1334            BucketIdentifier::FullName(bucket_name) => {
1335                self.get::<_, GetBucketResponse>(self.endpoints.bucket_by_name(&bucket_name)?)?
1336                    .bucket
1337            }
1338        })
1339    }
1340
1341    pub fn delete_bucket<IdentifierT>(&self, bucket: IdentifierT) -> Result<()>
1342    where
1343        IdentifierT: Into<BucketIdentifier>,
1344    {
1345        let bucket_id = match bucket.into() {
1346            BucketIdentifier::Id(bucket_id) => bucket_id,
1347            bucket @ BucketIdentifier::FullName(_) => self.get_bucket(bucket)?.id,
1348        };
1349        self.delete(self.endpoints.bucket_by_id(&bucket_id)?)
1350    }
1351
1352    pub fn fetch_stream_comments(
1353        &self,
1354        stream_name: &StreamFullName,
1355        size: u32,
1356    ) -> Result<StreamBatch> {
1357        self.post(
1358            self.endpoints.stream_fetch(stream_name)?,
1359            StreamFetchRequest { size },
1360            Retry::No,
1361        )
1362    }
1363
1364    pub fn get_stream(&self, stream_name: &StreamFullName) -> Result<Stream> {
1365        Ok(self
1366            .get::<_, GetStreamResponse>(self.endpoints.stream(stream_name)?)?
1367            .stream)
1368    }
1369
1370    pub fn advance_stream(
1371        &self,
1372        stream_name: &StreamFullName,
1373        sequence_id: StreamSequenceId,
1374    ) -> Result<()> {
1375        self.post::<_, _, serde::de::IgnoredAny>(
1376            self.endpoints.stream_advance(stream_name)?,
1377            StreamAdvanceRequest { sequence_id },
1378            Retry::No,
1379        )?;
1380        Ok(())
1381    }
1382
1383    pub fn reset_stream(
1384        &self,
1385        stream_name: &StreamFullName,
1386        to_comment_created_at: DateTime<Utc>,
1387    ) -> Result<()> {
1388        self.post::<_, _, serde::de::IgnoredAny>(
1389            self.endpoints.stream_reset(stream_name)?,
1390            StreamResetRequest {
1391                to_comment_created_at,
1392            },
1393            Retry::No,
1394        )?;
1395        Ok(())
1396    }
1397
1398    pub fn tag_stream_exceptions(
1399        &self,
1400        stream_name: &StreamFullName,
1401        exceptions: &[StreamException],
1402    ) -> Result<()> {
1403        self.put::<_, _, serde::de::IgnoredAny>(
1404            self.endpoints.stream_exceptions(stream_name)?,
1405            TagStreamExceptionsRequest { exceptions },
1406        )?;
1407        Ok(())
1408    }
1409
1410    /// Gets a project.
1411    pub fn get_project(&self, project_name: &ProjectName) -> Result<Project> {
1412        let response =
1413            self.get::<_, GetProjectResponse>(self.endpoints.project_by_name(project_name)?)?;
1414        Ok(response.project)
1415    }
1416
1417    /// Gets all projects.
1418    pub fn get_projects(&self) -> Result<Vec<Project>> {
1419        let response = self.get::<_, GetProjectsResponse>(self.endpoints.projects.clone())?;
1420        Ok(response.projects)
1421    }
1422
1423    /// Creates a new project.
1424    pub fn create_project(
1425        &self,
1426        project_name: &ProjectName,
1427        options: NewProject,
1428        user_ids: &[UserId],
1429    ) -> Result<Project> {
1430        Ok(self
1431            .put::<_, _, CreateProjectResponse>(
1432                self.endpoints.project_by_name(project_name)?,
1433                CreateProjectRequest {
1434                    project: options,
1435                    user_ids,
1436                },
1437            )?
1438            .project)
1439    }
1440
1441    /// Updates an existing project.
1442    pub fn update_project(
1443        &self,
1444        project_name: &ProjectName,
1445        options: UpdateProject,
1446    ) -> Result<Project> {
1447        Ok(self
1448            .post::<_, _, UpdateProjectResponse>(
1449                self.endpoints.project_by_name(project_name)?,
1450                UpdateProjectRequest { project: options },
1451                Retry::Yes,
1452            )?
1453            .project)
1454    }
1455
1456    /// Deletes an existing project.
1457    pub fn delete_project(
1458        &self,
1459        project_name: &ProjectName,
1460        force_delete: ForceDeleteProject,
1461    ) -> Result<()> {
1462        let endpoint = self.endpoints.project_by_name(project_name)?;
1463        match force_delete {
1464            ForceDeleteProject::No => self.delete(endpoint)?,
1465            ForceDeleteProject::Yes => {
1466                self.delete_query(endpoint, Some(&json!({ "force": true })))?
1467            }
1468        };
1469        Ok(())
1470    }
1471
1472    fn get<LocationT, SuccessT>(&self, url: LocationT) -> Result<SuccessT>
1473    where
1474        LocationT: IntoUrl + Display + Clone,
1475        for<'de> SuccessT: Deserialize<'de>,
1476    {
1477        self.request(&Method::GET, &url, &None::<()>, &None::<()>, &Retry::Yes)
1478    }
1479
1480    fn get_query<LocationT, QueryT, SuccessT>(
1481        &self,
1482        url: LocationT,
1483        query: Option<&QueryT>,
1484    ) -> Result<SuccessT>
1485    where
1486        LocationT: IntoUrl + Display + Clone,
1487        QueryT: Serialize,
1488        for<'de> SuccessT: Deserialize<'de>,
1489    {
1490        self.request(&Method::GET, &url, &None::<()>, &Some(query), &Retry::Yes)
1491    }
1492
1493    fn delete<LocationT>(&self, url: LocationT) -> Result<()>
1494    where
1495        LocationT: IntoUrl + Display + Clone,
1496    {
1497        self.delete_query::<LocationT, ()>(url, None)
1498    }
1499
1500    fn delete_query<LocationT, QueryT>(&self, url: LocationT, query: Option<&QueryT>) -> Result<()>
1501    where
1502        LocationT: IntoUrl + Display + Clone,
1503        QueryT: Serialize,
1504    {
1505        debug!("Attempting DELETE `{url}`");
1506
1507        let attempts = Cell::new(0);
1508        let http_response = self
1509            .with_retries(|| {
1510                attempts.set(attempts.get() + 1);
1511
1512                let mut request = self
1513                    .http_client
1514                    .delete(url.clone())
1515                    .headers(self.headers.clone());
1516                if let Some(query) = query {
1517                    request = request.query(query);
1518                }
1519                request.send()
1520            })
1521            .map_err(|source| Error::ReqwestError {
1522                source,
1523                message: "DELETE operation failed.".to_owned(),
1524            })?;
1525        let status = http_response.status();
1526        http_response
1527            .json::<Response<EmptySuccess>>()
1528            .map_err(Error::BadJsonResponse)?
1529            .into_result(status)
1530            .map_or_else(
1531                // Ignore 404 not found if the request had to be re-tried - assume the target
1532                // object was deleted on a previous incomplete request.
1533                |error| {
1534                    if attempts.get() > 1 && status == reqwest::StatusCode::NOT_FOUND {
1535                        Ok(())
1536                    } else {
1537                        Err(error)
1538                    }
1539                },
1540                |_| Ok(()),
1541            )
1542    }
1543
1544    fn post<LocationT, RequestT, SuccessT>(
1545        &self,
1546        url: LocationT,
1547        request: RequestT,
1548        retry: Retry,
1549    ) -> Result<SuccessT>
1550    where
1551        LocationT: IntoUrl + Display + Clone,
1552        RequestT: Serialize,
1553        for<'de> SuccessT: Deserialize<'de>,
1554    {
1555        self.request(&Method::POST, &url, &Some(request), &None::<()>, &retry)
1556    }
1557
1558    fn put<LocationT, RequestT, SuccessT>(
1559        &self,
1560        url: LocationT,
1561        request: RequestT,
1562    ) -> Result<SuccessT>
1563    where
1564        LocationT: IntoUrl + Display + Clone,
1565        RequestT: Serialize,
1566        for<'de> SuccessT: Deserialize<'de>,
1567    {
1568        self.request(&Method::PUT, &url, &Some(request), &None::<()>, &Retry::Yes)
1569    }
1570
1571    fn raw_request<LocationT, RequestT, QueryT>(
1572        &self,
1573        method: &Method,
1574        url: &LocationT,
1575        body: &Option<RequestT>,
1576        query: &Option<QueryT>,
1577        retry: &Retry,
1578        accept_header: Option<HeaderValue>,
1579    ) -> Result<reqwest::blocking::Response>
1580    where
1581        LocationT: IntoUrl + Display + Clone,
1582        RequestT: Serialize,
1583        QueryT: Serialize,
1584    {
1585        let mut headers = self.headers.clone();
1586
1587        if let Some(accept_header) = accept_header {
1588            headers.insert(ACCEPT, accept_header);
1589        }
1590
1591        let do_request = || {
1592            let request = self
1593                .http_client
1594                .request(method.clone(), url.clone())
1595                .headers(headers.clone());
1596
1597            let request = match &query {
1598                Some(query) => request.query(query),
1599                None => request,
1600            };
1601            let request = match &body {
1602                Some(body) => request.json(body),
1603                None => request,
1604            };
1605            request.send()
1606        };
1607
1608        let result = match retry {
1609            Retry::Yes => self.with_retries(do_request),
1610            Retry::No => do_request(),
1611        };
1612        let http_response = result.map_err(|source| Error::ReqwestError {
1613            source,
1614            message: format!("{method} operation failed."),
1615        })?;
1616
1617        Ok(http_response)
1618    }
1619
1620    fn splitable_request<LocationT, RequestT, SuccessT, QueryT>(
1621        &self,
1622        method: Method,
1623        url: LocationT,
1624        body: RequestT,
1625        query: Option<QueryT>,
1626        retry: Retry,
1627    ) -> SplitableRequestResponse<SuccessT>
1628    where
1629        LocationT: IntoUrl + Display + Clone,
1630        RequestT: Serialize + SplittableRequest + Clone,
1631        QueryT: Serialize + Clone,
1632        for<'de> SuccessT: Deserialize<'de> + ReducibleResponse + Clone + Default,
1633    {
1634        debug!("Attempting {method} `{url}`");
1635        let result: Result<SuccessT> =
1636            self.request(&method, &url, &Some(body.clone()), &query, &retry);
1637
1638        match result {
1639            Ok(response) => SplitableRequestResponse {
1640                response,
1641                num_failed: 0,
1642            },
1643            Err(_) => {
1644                let mut num_failed = 0;
1645                let response = body
1646                    .split()
1647                    .filter_map(|request| {
1648                        match self.request(&method, &url, &Some(request), &query, &retry) {
1649                            Ok(response) => Some(response),
1650                            Err(err) => {
1651                                debug!("{err}");
1652                                num_failed += 1;
1653                                None
1654                            }
1655                        }
1656                    })
1657                    .fold(SuccessT::empty(), |merged, next: SuccessT| {
1658                        merged.merge(next)
1659                    });
1660
1661                SplitableRequestResponse {
1662                    num_failed,
1663                    response,
1664                }
1665            }
1666        }
1667    }
1668
1669    fn request<LocationT, RequestT, SuccessT, QueryT>(
1670        &self,
1671        method: &Method,
1672        url: &LocationT,
1673        body: &Option<RequestT>,
1674        query: &Option<QueryT>,
1675        retry: &Retry,
1676    ) -> Result<SuccessT>
1677    where
1678        LocationT: IntoUrl + Display + Clone,
1679        RequestT: Serialize,
1680        QueryT: Serialize + Clone,
1681        for<'de> SuccessT: Deserialize<'de>,
1682    {
1683        debug!("Attempting {method} `{url}`");
1684        let http_response = self.raw_request(method, url, body, query, retry, None)?;
1685
1686        let status = http_response.status();
1687
1688        let response_text = http_response.text().map_err(|source| Error::ReqwestError {
1689            message: "Could not get request text".to_string(),
1690            source,
1691        })?;
1692
1693        let mut deserializer = serde_json::Deserializer::from_str(&response_text);
1694        deserializer.disable_recursion_limit();
1695
1696        Response::<SuccessT>::deserialize(&mut deserializer)
1697            .map_err(Error::BadSerdeJsonResponse)?
1698            .into_result(status)
1699    }
1700
1701    fn with_retries(
1702        &self,
1703        send_request: impl Fn() -> ReqwestResult<HttpResponse>,
1704    ) -> ReqwestResult<HttpResponse> {
1705        match &self.retrier {
1706            Some(retrier) => retrier.with_retries(send_request),
1707            None => send_request(),
1708        }
1709    }
1710}
1711
1712#[derive(Copy, Clone)]
1713enum Retry {
1714    Yes,
1715    No,
1716}
1717
1718pub struct DatasetQueryIter<'a> {
1719    client: &'a Client,
1720    dataset_name: &'a DatasetFullName,
1721    done: bool,
1722    params: &'a mut QueryRequestParams,
1723}
1724
1725impl<'a> DatasetQueryIter<'a> {
1726    fn new(
1727        client: &'a Client,
1728        dataset_name: &'a DatasetFullName,
1729        params: &'a mut QueryRequestParams,
1730    ) -> Self {
1731        Self {
1732            client,
1733            dataset_name,
1734            done: false,
1735            params,
1736        }
1737    }
1738}
1739
1740impl Iterator for DatasetQueryIter<'_> {
1741    type Item = Result<Vec<AnnotatedComment>>;
1742
1743    fn next(&mut self) -> Option<Self::Item> {
1744        if self.done {
1745            return None;
1746        }
1747
1748        let response = self.client.query_dataset(self.dataset_name, self.params);
1749        Some(response.map(|page| {
1750            self.params.continuation = page.continuation;
1751            self.done = self.params.continuation.is_none();
1752            page.results
1753        }))
1754    }
1755}
1756
1757pub enum ContinuationKind {
1758    Timestamp(DateTime<Utc>),
1759    Continuation(Continuation),
1760}
1761
1762pub struct EmailsIter<'a> {
1763    client: &'a Client,
1764    bucket_name: &'a BucketFullName,
1765    continuation: Option<EmailContinuation>,
1766    done: bool,
1767    page_size: usize,
1768}
1769
1770impl<'a> EmailsIter<'a> {
1771    // Default number of emails per page to request from API.
1772    pub const DEFAULT_PAGE_SIZE: usize = 64;
1773    // Maximum number of emails per page which can be requested from the API.
1774    pub const MAX_PAGE_SIZE: usize = 256;
1775
1776    fn new(client: &'a Client, bucket_name: &'a BucketFullName, page_size: Option<usize>) -> Self {
1777        Self {
1778            client,
1779            bucket_name,
1780            continuation: None,
1781            done: false,
1782            page_size: page_size.unwrap_or(Self::DEFAULT_PAGE_SIZE),
1783        }
1784    }
1785}
1786
1787impl Iterator for EmailsIter<'_> {
1788    type Item = Result<Vec<Email>>;
1789
1790    fn next(&mut self) -> Option<Self::Item> {
1791        if self.done {
1792            return None;
1793        }
1794        let response = self.client.get_emails_iter_page(
1795            self.bucket_name,
1796            self.continuation.as_ref(),
1797            self.page_size,
1798        );
1799        Some(response.map(|page| {
1800            self.continuation = page.continuation;
1801            self.done = self.continuation.is_none();
1802            page.emails
1803        }))
1804    }
1805}
1806
1807#[derive(Debug, Default)]
1808pub struct EmailsQueryFilter {
1809    pub from_timestamp: Option<DateTime<Utc>>,
1810    pub to_timestamp: Option<DateTime<Utc>>,
1811    pub mailbox_name: Option<String>,
1812}
1813
1814impl EmailsQueryFilter {
1815    pub fn is_empty(&self) -> bool {
1816        self.from_timestamp.is_none() && self.to_timestamp.is_none() && self.mailbox_name.is_none()
1817    }
1818}
1819
1820pub struct EmailsQueryIter<'a> {
1821    client: &'a Client,
1822    bucket_name: &'a BucketFullName,
1823    filter: EmailsQueryFilter,
1824    continuation: Option<EmailContinuation>,
1825    done: bool,
1826    page_size: usize,
1827}
1828
1829impl<'a> EmailsQueryIter<'a> {
1830    // Default number of emails per page to request from API.
1831    pub const DEFAULT_PAGE_SIZE: usize = 64;
1832
1833    fn new(
1834        client: &'a Client,
1835        bucket_name: &'a BucketFullName,
1836        filter: EmailsQueryFilter,
1837        page_size: Option<usize>,
1838    ) -> Self {
1839        Self {
1840            client,
1841            bucket_name,
1842            filter,
1843            continuation: None,
1844            done: false,
1845            page_size: page_size.unwrap_or(Self::DEFAULT_PAGE_SIZE),
1846        }
1847    }
1848}
1849
1850impl Iterator for EmailsQueryIter<'_> {
1851    type Item = Result<Vec<Email>>;
1852
1853    fn next(&mut self) -> Option<Self::Item> {
1854        if self.done {
1855            return None;
1856        }
1857        let response = self.client.query_emails_iter_page(
1858            self.bucket_name,
1859            &self.filter,
1860            self.continuation.as_ref(),
1861            self.page_size,
1862        );
1863        Some(response.map(|page| {
1864            self.continuation = page.continuation;
1865            // Terminate on `more_results`, not page size: under outline-mode
1866            // mailbox filtering a page can be shorter than `limit` while more
1867            // matches remain further in the bucket.
1868            self.done = !page.more_results;
1869            page.emails
1870        }))
1871    }
1872}
1873
1874pub struct CommentsIter<'a> {
1875    client: &'a Client,
1876    source_name: &'a SourceFullName,
1877    continuation: Option<ContinuationKind>,
1878    done: bool,
1879    page_size: usize,
1880    to_timestamp: Option<DateTime<Utc>>,
1881}
1882
1883#[derive(Debug, Default)]
1884pub struct CommentsIterTimerange {
1885    pub from: Option<DateTime<Utc>>,
1886    pub to: Option<DateTime<Utc>>,
1887}
1888impl<'a> CommentsIter<'a> {
1889    // Default number of comments per page to request from API.
1890    pub const DEFAULT_PAGE_SIZE: usize = 64;
1891    // Maximum number of comments per page which can be requested from the API.
1892    pub const MAX_PAGE_SIZE: usize = 256;
1893
1894    fn new(
1895        client: &'a Client,
1896        source_name: &'a SourceFullName,
1897        page_size: Option<usize>,
1898        timerange: CommentsIterTimerange,
1899    ) -> Self {
1900        let (from_timestamp, to_timestamp) = (timerange.from, timerange.to);
1901        Self {
1902            client,
1903            source_name,
1904            to_timestamp,
1905            continuation: from_timestamp.map(ContinuationKind::Timestamp),
1906            done: false,
1907            page_size: page_size.unwrap_or(Self::DEFAULT_PAGE_SIZE),
1908        }
1909    }
1910}
1911
1912impl Iterator for CommentsIter<'_> {
1913    type Item = Result<Vec<Comment>>;
1914
1915    fn next(&mut self) -> Option<Self::Item> {
1916        if self.done {
1917            return None;
1918        }
1919        let response = self.client.get_comments_iter_page(
1920            self.source_name,
1921            self.continuation.as_ref(),
1922            self.to_timestamp,
1923            self.page_size,
1924        );
1925        Some(response.map(|page| {
1926            self.continuation = page.continuation.map(ContinuationKind::Continuation);
1927            self.done = self.continuation.is_none();
1928            page.comments
1929        }))
1930    }
1931}
1932
1933pub struct LabellingsIter<'a> {
1934    client: &'a Client,
1935    dataset_name: &'a DatasetFullName,
1936    source_id: &'a SourceId,
1937    return_predictions: bool,
1938    after: Option<GetLabellingsAfter>,
1939    limit: Option<usize>,
1940    done: bool,
1941}
1942
1943impl<'a> LabellingsIter<'a> {
1944    fn new(
1945        client: &'a Client,
1946        dataset_name: &'a DatasetFullName,
1947        source_id: &'a SourceId,
1948        return_predictions: bool,
1949        limit: Option<usize>,
1950    ) -> Self {
1951        Self {
1952            client,
1953            dataset_name,
1954            source_id,
1955            return_predictions,
1956            after: None,
1957            limit,
1958            done: false,
1959        }
1960    }
1961}
1962
1963impl Iterator for LabellingsIter<'_> {
1964    type Item = Result<Vec<AnnotatedComment>>;
1965
1966    fn next(&mut self) -> Option<Self::Item> {
1967        if self.done {
1968            return None;
1969        }
1970        let response = self.client.get_labellings_in_bulk(
1971            self.dataset_name,
1972            GetLabellingsInBulk {
1973                source_id: self.source_id,
1974                return_predictions: &self.return_predictions,
1975                after: &self.after,
1976                limit: &self.limit,
1977            },
1978        );
1979        Some(response.map(|page| {
1980            if self.after == page.after && !page.results.is_empty() {
1981                panic!("Labellings API did not increment pagination continuation");
1982            }
1983            self.after = page.after;
1984            if page.results.is_empty() {
1985                self.done = true;
1986            }
1987            page.results
1988        }))
1989    }
1990}
1991
1992#[derive(Debug)]
1993struct Endpoints {
1994    base: Url,
1995    datasets: Url,
1996    sources: Url,
1997    buckets: Url,
1998    users: Url,
1999    current_user: Url,
2000    projects: Url,
2001}
2002
2003#[derive(Debug, Serialize, Clone, Copy)]
2004struct NoChargeQuery {
2005    no_charge: bool,
2006}
2007
2008fn construct_endpoint(base: &Url, segments: &[&str]) -> Result<Url> {
2009    let mut endpoint = base.clone();
2010
2011    let mut endpoint_segments = endpoint
2012        .path_segments_mut()
2013        .map_err(|_| Error::BadEndpoint {
2014            endpoint: base.clone(),
2015        })?;
2016
2017    for segment in segments {
2018        endpoint_segments.push(segment);
2019    }
2020
2021    drop(endpoint_segments);
2022
2023    Ok(endpoint)
2024}
2025
2026impl Endpoints {
2027    pub fn new(base: Url) -> Result<Self> {
2028        let datasets = construct_endpoint(&base, &["api", "v1", "datasets"])?;
2029        let sources = construct_endpoint(&base, &["api", "v1", "sources"])?;
2030        let buckets = construct_endpoint(&base, &["api", "_private", "buckets"])?;
2031        let users = construct_endpoint(&base, &["api", "_private", "users"])?;
2032        let current_user = construct_endpoint(&base, &["auth", "user"])?;
2033        let projects = construct_endpoint(&base, &["api", "_private", "projects"])?;
2034
2035        Ok(Endpoints {
2036            base,
2037            datasets,
2038            sources,
2039            buckets,
2040            users,
2041            current_user,
2042            projects,
2043        })
2044    }
2045
2046    fn refresh_user_permissions(&self) -> Result<Url> {
2047        construct_endpoint(&self.base, &["auth", "refresh-user-permissions"])
2048    }
2049
2050    fn label_group(
2051        &self,
2052        dataset_name: &DatasetFullName,
2053        label_group: LabelGroupName,
2054    ) -> Result<Url> {
2055        construct_endpoint(
2056            &self.base,
2057            &[
2058                "api",
2059                "_private",
2060                "datasets",
2061                &dataset_name.0,
2062                "labels",
2063                &label_group.0,
2064            ],
2065        )
2066    }
2067
2068    fn ixp_datasets(&self) -> Result<Url> {
2069        construct_endpoint(&self.base, &["api", "_private", "ixp", "datasets"])
2070    }
2071
2072    fn ixp_documents(&self, source_id: &SourceId) -> Result<Url> {
2073        construct_endpoint(
2074            &self.base,
2075            &[
2076                "api",
2077                "_private",
2078                "sources",
2079                &format!("id:{0}", source_id.0),
2080                "documents",
2081            ],
2082        )
2083    }
2084
2085    fn ixp_document(&self, source_id: &SourceId, comment_id: &CommentId) -> Result<Url> {
2086        construct_endpoint(
2087            &self.base,
2088            &[
2089                "api",
2090                "_private",
2091                "sources",
2092                &format!("id:{0}", source_id.0),
2093                "documents",
2094                &comment_id.0,
2095            ],
2096        )
2097    }
2098
2099    fn keyed_sync_states(&self, bucket_id: &BucketId) -> Result<Url> {
2100        construct_endpoint(
2101            &self.base,
2102            &[
2103                "api",
2104                "_private",
2105                "buckets",
2106                &format!("id:{}", bucket_id.0),
2107                "keyed-sync-states/",
2108            ],
2109        )
2110    }
2111
2112    fn keyed_sync_state(&self, bucket_id: &BucketId, id: &KeyedSyncStateId) -> Result<Url> {
2113        construct_endpoint(
2114            &self.base,
2115            &[
2116                "api",
2117                "_private",
2118                "buckets",
2119                &format!("id:{}", bucket_id.0),
2120                "keyed-sync-state",
2121                &id.0,
2122            ],
2123        )
2124    }
2125
2126    fn query_keyed_sync_state_ids(&self, bucket_id: &BucketId) -> Result<Url> {
2127        construct_endpoint(
2128            &self.base,
2129            &[
2130                "api",
2131                "_private",
2132                "buckets",
2133                &format!("id:{}", bucket_id.0),
2134                "keyed-sync-state-ids",
2135            ],
2136        )
2137    }
2138
2139    fn audit_events_query(&self) -> Result<Url> {
2140        construct_endpoint(&self.base, &["api", "v1", "audit_events", "query"])
2141    }
2142
2143    fn integrations(&self) -> Result<Url> {
2144        construct_endpoint(&self.base, &["api", "_private", "integrations"])
2145    }
2146
2147    fn integration(&self, name: &IntegrationFullName) -> Result<Url> {
2148        construct_endpoint(&self.base, &["api", "_private", "integrations", &name.0])
2149    }
2150
2151    fn attachment_reference(&self, reference: &AttachmentReference) -> Result<Url> {
2152        construct_endpoint(&self.base, &["api", "v1", "attachments", &reference.0])
2153    }
2154
2155    fn attachment_upload(
2156        &self,
2157        source_id: &SourceId,
2158        comment_id: &CommentId,
2159        attachment_index: usize,
2160    ) -> Result<Url> {
2161        construct_endpoint(
2162            &self.base,
2163            &[
2164                "api",
2165                "_private",
2166                "sources",
2167                &format!("id:{}", source_id.0),
2168                "comments",
2169                &comment_id.0,
2170                "attachments",
2171                &attachment_index.to_string(),
2172            ],
2173        )
2174    }
2175    fn latest_validation(&self, dataset_name: &DatasetFullName) -> Result<Url> {
2176        construct_endpoint(
2177            &self.base,
2178            &[
2179                "api",
2180                "_private",
2181                "datasets",
2182                &dataset_name.0,
2183                "labellers",
2184                "latest",
2185                "validation",
2186            ],
2187        )
2188    }
2189
2190    fn validation(
2191        &self,
2192        dataset_name: &DatasetFullName,
2193        model_version: &ModelVersion,
2194    ) -> Result<Url> {
2195        construct_endpoint(
2196            &self.base,
2197            &[
2198                "api",
2199                "_private",
2200                "datasets",
2201                &dataset_name.0,
2202                "labellers",
2203                &model_version.0.to_string(),
2204                "validation",
2205            ],
2206        )
2207    }
2208
2209    fn label_validation(
2210        &self,
2211        dataset_name: &DatasetFullName,
2212        model_version: &ModelVersion,
2213    ) -> Result<Url> {
2214        construct_endpoint(
2215            &self.base,
2216            &[
2217                "api",
2218                "_private",
2219                "datasets",
2220                &dataset_name.0,
2221                "labellers",
2222                &model_version.0.to_string(),
2223                "label-validation",
2224            ],
2225        )
2226    }
2227    fn bucket_statistics(&self, bucket_name: &BucketFullName) -> Result<Url> {
2228        construct_endpoint(
2229            &self.base,
2230            &["api", "_private", "buckets", &bucket_name.0, "statistics"],
2231        )
2232    }
2233
2234    fn dataset_summary(&self, dataset_name: &DatasetFullName) -> Result<Url> {
2235        construct_endpoint(
2236            &self.base,
2237            &["api", "_private", "datasets", &dataset_name.0, "summary"],
2238        )
2239    }
2240
2241    fn query_dataset(&self, dataset_name: &DatasetFullName) -> Result<Url> {
2242        construct_endpoint(
2243            &self.base,
2244            &["api", "_private", "datasets", &dataset_name.0, "query"],
2245        )
2246    }
2247
2248    fn streams(&self, dataset_name: &DatasetFullName) -> Result<Url> {
2249        construct_endpoint(
2250            &self.base,
2251            &["api", "v1", "datasets", &dataset_name.0, "streams"],
2252        )
2253    }
2254
2255    fn stream(&self, stream_name: &StreamFullName) -> Result<Url> {
2256        construct_endpoint(
2257            &self.base,
2258            &[
2259                "api",
2260                "v1",
2261                "datasets",
2262                &stream_name.dataset.0,
2263                "streams",
2264                &stream_name.stream.0,
2265            ],
2266        )
2267    }
2268
2269    fn stream_fetch(&self, stream_name: &StreamFullName) -> Result<Url> {
2270        construct_endpoint(
2271            &self.base,
2272            &[
2273                "api",
2274                "v1",
2275                "datasets",
2276                &stream_name.dataset.0,
2277                "streams",
2278                &stream_name.stream.0,
2279                "fetch",
2280            ],
2281        )
2282    }
2283
2284    fn stream_advance(&self, stream_name: &StreamFullName) -> Result<Url> {
2285        construct_endpoint(
2286            &self.base,
2287            &[
2288                "api",
2289                "v1",
2290                "datasets",
2291                &stream_name.dataset.0,
2292                "streams",
2293                &stream_name.stream.0,
2294                "advance",
2295            ],
2296        )
2297    }
2298
2299    fn stream_reset(&self, stream_name: &StreamFullName) -> Result<Url> {
2300        construct_endpoint(
2301            &self.base,
2302            &[
2303                "api",
2304                "v1",
2305                "datasets",
2306                &stream_name.dataset.0,
2307                "streams",
2308                &stream_name.stream.0,
2309                "reset",
2310            ],
2311        )
2312    }
2313
2314    fn stream_exceptions(&self, stream_name: &StreamFullName) -> Result<Url> {
2315        construct_endpoint(
2316            &self.base,
2317            &[
2318                "api",
2319                "v1",
2320                "datasets",
2321                &stream_name.dataset.0,
2322                "streams",
2323                &stream_name.stream.0,
2324                "exceptions",
2325            ],
2326        )
2327    }
2328
2329    fn recent_comments(&self, dataset_name: &DatasetFullName) -> Result<Url> {
2330        construct_endpoint(
2331            &self.base,
2332            &["api", "_private", "datasets", &dataset_name.0, "recent"],
2333        )
2334    }
2335
2336    fn dataset_statistics(&self, dataset_name: &DatasetFullName) -> Result<Url> {
2337        construct_endpoint(
2338            &self.base,
2339            &["api", "_private", "datasets", &dataset_name.0, "statistics"],
2340        )
2341    }
2342
2343    fn source_statistics(&self, source_name: &SourceFullName) -> Result<Url> {
2344        construct_endpoint(
2345            &self.base,
2346            &["api", "v1", "sources", &source_name.0, "statistics"],
2347        )
2348    }
2349
2350    fn user_by_id(&self, user_id: &UserId) -> Result<Url> {
2351        construct_endpoint(&self.base, &["api", "_private", "users", &user_id.0])
2352    }
2353
2354    fn source_by_id(&self, source_id: &SourceId) -> Result<Url> {
2355        construct_endpoint(
2356            &self.base,
2357            &["api", "v1", "sources", &format!("id:{}", source_id.0)],
2358        )
2359    }
2360
2361    fn source_by_name(&self, source_name: &SourceFullName) -> Result<Url> {
2362        construct_endpoint(&self.base, &["api", "v1", "sources", &source_name.0])
2363    }
2364
2365    fn quotas(&self, tenant_id: &Option<UiPathTenantId>) -> Result<Url> {
2366        if let Some(tenant_id) = tenant_id {
2367            construct_endpoint(&self.base, &["api", "_private", "quotas", &tenant_id.0])
2368        } else {
2369            construct_endpoint(&self.base, &["api", "_private", "quotas"])
2370        }
2371    }
2372
2373    fn quota(&self, tenant_id: &TenantId, tenant_quota_kind: TenantQuotaKind) -> Result<Url> {
2374        construct_endpoint(
2375            &self.base,
2376            &[
2377                "api",
2378                "_private",
2379                "quotas",
2380                &tenant_id.to_string(),
2381                &tenant_quota_kind.to_string(),
2382            ],
2383        )
2384    }
2385
2386    fn put_comments(&self, source_name: &SourceFullName) -> Result<Url> {
2387        construct_endpoint(
2388            &self.base,
2389            &["api", "_private", "sources", &source_name.0, "comments"],
2390        )
2391    }
2392
2393    fn comments(&self, source_name: &SourceFullName) -> Result<Url> {
2394        construct_endpoint(
2395            &self.base,
2396            &["api", "_private", "sources", &source_name.0, "comments"],
2397        )
2398    }
2399
2400    fn comment_by_id(&self, source_name: &SourceFullName, comment_id: &CommentId) -> Result<Url> {
2401        construct_endpoint(
2402            &self.base,
2403            &[
2404                "api",
2405                "v1",
2406                "sources",
2407                &source_name.0,
2408                "comments",
2409                &comment_id.0,
2410            ],
2411        )
2412    }
2413
2414    fn comments_v1(&self, source_name: &SourceFullName) -> Result<Url> {
2415        construct_endpoint(
2416            &self.base,
2417            &["api", "v1", "sources", &source_name.0, "comments"],
2418        )
2419    }
2420
2421    fn sync_comments(&self, source_name: &SourceFullName) -> Result<Url> {
2422        construct_endpoint(
2423            &self.base,
2424            &["api", "v1", "sources", &source_name.0, "sync"],
2425        )
2426    }
2427
2428    fn sync_comments_raw_emails(&self, source_name: &SourceFullName) -> Result<Url> {
2429        construct_endpoint(
2430            &self.base,
2431            &["api", "v1", "sources", &source_name.0, "sync-raw-emails"],
2432        )
2433    }
2434
2435    fn comment_audio(&self, source_id: &SourceId, comment_id: &CommentId) -> Result<Url> {
2436        construct_endpoint(
2437            &self.base,
2438            &[
2439                "api",
2440                "_private",
2441                "sources",
2442                &format!("id:{}", source_id.0),
2443                "comments",
2444                &comment_id.0,
2445                "audio",
2446            ],
2447        )
2448    }
2449
2450    fn get_emails(&self, bucket_name: &BucketFullName) -> Result<Url> {
2451        construct_endpoint(
2452            &self.base,
2453            &["api", "_private", "buckets", &bucket_name.0, "emails"],
2454        )
2455    }
2456
2457    fn query_emails(&self, bucket_name: &BucketFullName) -> Result<Url> {
2458        construct_endpoint(
2459            &self.base,
2460            &[
2461                "api",
2462                "_private",
2463                "buckets",
2464                &bucket_name.0,
2465                "emails",
2466                "query",
2467            ],
2468        )
2469    }
2470
2471    fn put_emails(&self, bucket_name: &BucketFullName) -> Result<Url> {
2472        construct_endpoint(
2473            &self.base,
2474            &["api", "_private", "buckets", &bucket_name.0, "emails"],
2475        )
2476    }
2477
2478    fn delete_emails(&self, bucket_name: &BucketFullName) -> Result<Url> {
2479        construct_endpoint(
2480            &self.base,
2481            &["api", "_private", "buckets", &bucket_name.0, "emails"],
2482        )
2483    }
2484
2485    fn post_user(&self, user_id: &UserId) -> Result<Url> {
2486        construct_endpoint(&self.base, &["api", "_private", "users", &user_id.0])
2487    }
2488
2489    fn dataset_by_id(&self, dataset_id: &DatasetId) -> Result<Url> {
2490        construct_endpoint(
2491            &self.base,
2492            &["api", "v1", "datasets", &format!("id:{}", dataset_id.0)],
2493        )
2494    }
2495
2496    fn dataset_by_name(&self, dataset_name: &DatasetFullName) -> Result<Url> {
2497        construct_endpoint(&self.base, &["api", "v1", "datasets", &dataset_name.0])
2498    }
2499
2500    fn get_labellings(&self, dataset_name: &DatasetFullName) -> Result<Url> {
2501        construct_endpoint(
2502            &self.base,
2503            &["api", "_private", "datasets", &dataset_name.0, "labellings"],
2504        )
2505    }
2506
2507    fn labellers(&self, dataset_name: &DatasetFullName) -> Result<Url> {
2508        construct_endpoint(
2509            &self.base,
2510            &["api", "_private", "datasets", &dataset_name.0, "labellers"],
2511        )
2512    }
2513
2514    fn get_comment_predictions(
2515        &self,
2516        dataset_name: &DatasetFullName,
2517        model_version: &ModelVersion,
2518    ) -> Result<Url> {
2519        construct_endpoint(
2520            &self.base,
2521            &[
2522                "api",
2523                "v1",
2524                "datasets",
2525                &dataset_name.0,
2526                "labellers",
2527                &model_version.0.to_string(),
2528                "predict-comments",
2529            ],
2530        )
2531    }
2532
2533    fn post_labelling(
2534        &self,
2535        dataset_name: &DatasetFullName,
2536        comment_uid: &CommentUid,
2537    ) -> Result<Url> {
2538        construct_endpoint(
2539            &self.base,
2540            &[
2541                "api",
2542                "_private",
2543                "datasets",
2544                &dataset_name.0,
2545                "labellings",
2546                &comment_uid.0,
2547            ],
2548        )
2549    }
2550
2551    fn bucket_by_id(&self, bucket_id: &BucketId) -> Result<Url> {
2552        construct_endpoint(
2553            &self.base,
2554            &["api", "_private", "buckets", &format!("id:{}", bucket_id.0)],
2555        )
2556    }
2557
2558    fn bucket_by_name(&self, bucket_name: &BucketFullName) -> Result<Url> {
2559        construct_endpoint(&self.base, &["api", "_private", "buckets", &bucket_name.0])
2560    }
2561
2562    fn project_by_name(&self, project_name: &ProjectName) -> Result<Url> {
2563        construct_endpoint(
2564            &self.base,
2565            &["api", "_private", "projects", &project_name.0],
2566        )
2567    }
2568
2569    fn welcome_email(&self, user_id: &UserId) -> Result<Url> {
2570        construct_endpoint(
2571            &self.base,
2572            &["api", "_private", "users", &user_id.0, "welcome-email"],
2573        )
2574    }
2575}
2576
2577const DEFAULT_HTTP_TIMEOUT_SECONDS: u64 = 240;
2578
2579fn build_http_client(config: &Config) -> Result<HttpClient> {
2580    let mut builder = HttpClient::builder()
2581        .gzip(true)
2582        .danger_accept_invalid_certs(config.accept_invalid_certificates)
2583        .timeout(Some(Duration::from_secs(DEFAULT_HTTP_TIMEOUT_SECONDS)));
2584
2585    if let Some(proxy) = config.proxy.clone() {
2586        builder = builder.proxy(Proxy::all(proxy).map_err(Error::BuildHttpClient)?);
2587    }
2588    builder.build().map_err(Error::BuildHttpClient)
2589}
2590
2591fn build_headers(config: &Config) -> Result<HeaderMap> {
2592    let mut headers = HeaderMap::new();
2593    headers.insert(
2594        header::AUTHORIZATION,
2595        HeaderValue::from_str(&format!("Bearer {}", config.token.0)).map_err(|_| {
2596            Error::BadToken {
2597                token: config.token.0.clone(),
2598            }
2599        })?,
2600    );
2601    Ok(headers)
2602}
2603
2604fn id_list_query<'a>(ids: impl Iterator<Item = &'a String>) -> Vec<(&'static str, &'a str)> {
2605    // Return a list of pairs ("id", "a"), ("id", "b"), ...
2606    // The http client will turn this into a query string of
2607    // the form "id=a&id=b&..."
2608    ids.map(|id| ("id", id.as_str())).collect()
2609}
2610
2611pub static DEFAULT_ENDPOINT: Lazy<Url> =
2612    Lazy::new(|| Url::parse("https://reinfer.dev").expect("Default URL is well-formed"));
2613
2614#[cfg(test)]
2615mod tests {
2616    use super::*;
2617
2618    #[test]
2619    fn test_construct_endpoint() {
2620        let url = construct_endpoint(
2621            &Url::parse("https://cloud.uipath.com/org/tenant/reinfer_").unwrap(),
2622            &["api", "v1", "sources", "project", "source", "sync"],
2623        )
2624        .unwrap();
2625
2626        assert_eq!(
2627            url.to_string(),
2628            "https://cloud.uipath.com/org/tenant/reinfer_/api/v1/sources/project/source/sync"
2629        )
2630    }
2631
2632    #[test]
2633    fn test_id_list_query() {
2634        assert_eq!(id_list_query(Vec::new().iter()), Vec::new());
2635        assert_eq!(
2636            id_list_query(["foo".to_owned()].iter()),
2637            vec![("id", "foo")]
2638        );
2639        assert_eq!(
2640            id_list_query(
2641                [
2642                    "Stream".to_owned(),
2643                    "River".to_owned(),
2644                    "Waterfall".to_owned()
2645                ]
2646                .iter()
2647            ),
2648            [("id", "Stream"), ("id", "River"), ("id", "Waterfall"),]
2649        );
2650    }
2651}