edgefirst_client/
api.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright © 2025 Au-Zone Technologies. All Rights Reserved.
3
4use crate::{AnnotationSet, Client, Dataset, Error, Sample, client};
5use chrono::{DateTime, Utc};
6use log::trace;
7use reqwest::multipart::{Form, Part};
8use serde::{Deserialize, Deserializer, Serialize};
9use std::{collections::HashMap, fmt::Display, path::PathBuf, str::FromStr};
10
11/// Generic parameter value used in API requests and configuration.
12///
13/// This enum represents various data types that can be passed as parameters
14/// to EdgeFirst Studio API calls or stored in configuration files.
15///
16/// # Examples
17///
18/// ```rust
19/// use edgefirst_client::Parameter;
20/// use std::collections::HashMap;
21///
22/// // Different parameter types
23/// let int_param = Parameter::Integer(42);
24/// let float_param = Parameter::Real(3.14);
25/// let bool_param = Parameter::Boolean(true);
26/// let string_param = Parameter::String("model_name".to_string());
27///
28/// // Complex nested parameters
29/// let array_param = Parameter::Array(vec![
30///     Parameter::Integer(1),
31///     Parameter::Integer(2),
32///     Parameter::Integer(3),
33/// ]);
34///
35/// let mut config = HashMap::new();
36/// config.insert("learning_rate".to_string(), Parameter::Real(0.001));
37/// config.insert("epochs".to_string(), Parameter::Integer(100));
38/// let object_param = Parameter::Object(config);
39/// ```
40#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
41#[serde(untagged)]
42pub enum Parameter {
43    /// 64-bit signed integer value.
44    Integer(i64),
45    /// 64-bit floating-point value.
46    Real(f64),
47    /// Boolean true/false value.
48    Boolean(bool),
49    /// UTF-8 string value.
50    String(String),
51    /// Array of nested parameter values.
52    Array(Vec<Parameter>),
53    /// Object/map with string keys and parameter values.
54    Object(HashMap<String, Parameter>),
55}
56
57#[derive(Deserialize)]
58pub struct LoginResult {
59    pub(crate) token: String,
60}
61
62/// Unique identifier for an organization in EdgeFirst Studio.
63///
64/// Organizations are the top-level containers for users, projects, and
65/// resources in EdgeFirst Studio. Each organization has a unique ID that is
66/// displayed in hexadecimal format with an "org-" prefix (e.g., "org-abc123").
67///
68/// # Examples
69///
70/// ```rust
71/// use edgefirst_client::OrganizationID;
72///
73/// // Create from u64
74/// let org_id = OrganizationID::from(12345);
75/// println!("{}", org_id); // Displays: org-3039
76///
77/// // Parse from string
78/// let org_id: OrganizationID = "org-abc123".try_into().unwrap();
79/// assert_eq!(org_id.value(), 0xabc123);
80/// ```
81#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
82pub struct OrganizationID(u64);
83
84impl Display for OrganizationID {
85    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
86        write!(f, "org-{:x}", self.0)
87    }
88}
89
90impl From<u64> for OrganizationID {
91    fn from(id: u64) -> Self {
92        OrganizationID(id)
93    }
94}
95
96impl From<OrganizationID> for u64 {
97    fn from(val: OrganizationID) -> Self {
98        val.0
99    }
100}
101
102impl OrganizationID {
103    pub fn value(&self) -> u64 {
104        self.0
105    }
106}
107
108impl TryFrom<&str> for OrganizationID {
109    type Error = Error;
110
111    fn try_from(s: &str) -> Result<Self, Self::Error> {
112        let hex_part = s.strip_prefix("org-").ok_or_else(|| {
113            Error::InvalidParameters("Organization ID must start with 'org-' prefix".to_string())
114        })?;
115        let id = u64::from_str_radix(hex_part, 16)?;
116        Ok(OrganizationID(id))
117    }
118}
119
120/// Organization information and metadata.
121///
122/// Each user belongs to an organization which contains projects, datasets,
123/// and other resources. Organizations provide isolated workspaces for teams
124/// and manage resource quotas and billing.
125///
126/// # Examples
127///
128/// ```no_run
129/// use edgefirst_client::{Client, Organization};
130///
131/// # async fn example() -> Result<(), edgefirst_client::Error> {
132/// # let client = Client::new()?;
133/// // Access organization details
134/// let org: Organization = client.organization().await?;
135/// println!("Organization: {} (ID: {})", org.name(), org.id());
136/// println!("Available credits: {}", org.credits());
137/// # Ok(())
138/// # }
139/// ```
140#[derive(Deserialize, Clone, Debug)]
141pub struct Organization {
142    id: OrganizationID,
143    name: String,
144    #[serde(rename = "latest_credit")]
145    credits: i64,
146}
147
148impl Display for Organization {
149    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
150        write!(f, "{}", self.name())
151    }
152}
153
154impl Organization {
155    pub fn id(&self) -> OrganizationID {
156        self.id
157    }
158
159    pub fn name(&self) -> &str {
160        &self.name
161    }
162
163    pub fn credits(&self) -> i64 {
164        self.credits
165    }
166}
167
168/// Unique identifier for a project within EdgeFirst Studio.
169///
170/// Projects contain datasets, experiments, and models within an organization.
171/// Each project has a unique ID displayed in hexadecimal format with a "p-"
172/// prefix (e.g., "p-def456").
173///
174/// # Examples
175///
176/// ```rust
177/// use edgefirst_client::ProjectID;
178/// use std::str::FromStr;
179///
180/// // Create from u64
181/// let project_id = ProjectID::from(78910);
182/// println!("{}", project_id); // Displays: p-1343e
183///
184/// // Parse from string
185/// let project_id = ProjectID::from_str("p-def456").unwrap();
186/// assert_eq!(project_id.value(), 0xdef456);
187/// ```
188#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
189pub struct ProjectID(u64);
190
191impl Display for ProjectID {
192    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
193        write!(f, "p-{:x}", self.0)
194    }
195}
196
197impl From<u64> for ProjectID {
198    fn from(id: u64) -> Self {
199        ProjectID(id)
200    }
201}
202
203impl From<ProjectID> for u64 {
204    fn from(val: ProjectID) -> Self {
205        val.0
206    }
207}
208
209impl ProjectID {
210    pub fn value(&self) -> u64 {
211        self.0
212    }
213}
214
215impl TryFrom<&str> for ProjectID {
216    type Error = Error;
217
218    fn try_from(s: &str) -> Result<Self, Self::Error> {
219        ProjectID::from_str(s)
220    }
221}
222
223impl TryFrom<String> for ProjectID {
224    type Error = Error;
225
226    fn try_from(s: String) -> Result<Self, Self::Error> {
227        ProjectID::from_str(&s)
228    }
229}
230
231impl FromStr for ProjectID {
232    type Err = Error;
233
234    fn from_str(s: &str) -> Result<Self, Self::Err> {
235        let hex_part = s.strip_prefix("p-").ok_or_else(|| {
236            Error::InvalidParameters("Project ID must start with 'p-' prefix".to_string())
237        })?;
238        let id = u64::from_str_radix(hex_part, 16)?;
239        Ok(ProjectID(id))
240    }
241}
242
243/// Unique identifier for an experiment within a project.
244///
245/// Experiments represent individual machine learning experiments with specific
246/// configurations, datasets, and results. Each experiment has a unique ID
247/// displayed in hexadecimal format with an "exp-" prefix (e.g., "exp-123abc").
248///
249/// # Examples
250///
251/// ```rust
252/// use edgefirst_client::ExperimentID;
253/// use std::str::FromStr;
254///
255/// // Create from u64
256/// let exp_id = ExperimentID::from(1193046);
257/// println!("{}", exp_id); // Displays: exp-123abc
258///
259/// // Parse from string
260/// let exp_id = ExperimentID::from_str("exp-456def").unwrap();
261/// assert_eq!(exp_id.value(), 0x456def);
262/// ```
263#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
264pub struct ExperimentID(u64);
265
266impl Display for ExperimentID {
267    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
268        write!(f, "exp-{:x}", self.0)
269    }
270}
271
272impl From<u64> for ExperimentID {
273    fn from(id: u64) -> Self {
274        ExperimentID(id)
275    }
276}
277
278impl From<ExperimentID> for u64 {
279    fn from(val: ExperimentID) -> Self {
280        val.0
281    }
282}
283
284impl ExperimentID {
285    pub fn value(&self) -> u64 {
286        self.0
287    }
288}
289
290impl TryFrom<&str> for ExperimentID {
291    type Error = Error;
292
293    fn try_from(s: &str) -> Result<Self, Self::Error> {
294        ExperimentID::from_str(s)
295    }
296}
297
298impl TryFrom<String> for ExperimentID {
299    type Error = Error;
300
301    fn try_from(s: String) -> Result<Self, Self::Error> {
302        ExperimentID::from_str(&s)
303    }
304}
305
306impl FromStr for ExperimentID {
307    type Err = Error;
308
309    fn from_str(s: &str) -> Result<Self, Self::Err> {
310        let hex_part = s.strip_prefix("exp-").ok_or_else(|| {
311            Error::InvalidParameters("Experiment ID must start with 'exp-' prefix".to_string())
312        })?;
313        let id = u64::from_str_radix(hex_part, 16)?;
314        Ok(ExperimentID(id))
315    }
316}
317
318/// Unique identifier for a training session within an experiment.
319///
320/// Training sessions represent individual training runs with specific
321/// hyperparameters and configurations. Each training session has a unique ID
322/// displayed in hexadecimal format with a "t-" prefix (e.g., "t-789012").
323///
324/// # Examples
325///
326/// ```rust
327/// use edgefirst_client::TrainingSessionID;
328/// use std::str::FromStr;
329///
330/// // Create from u64
331/// let training_id = TrainingSessionID::from(7901234);
332/// println!("{}", training_id); // Displays: t-7872f2
333///
334/// // Parse from string
335/// let training_id = TrainingSessionID::from_str("t-abc123").unwrap();
336/// assert_eq!(training_id.value(), 0xabc123);
337/// ```
338#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
339pub struct TrainingSessionID(u64);
340
341impl Display for TrainingSessionID {
342    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
343        write!(f, "t-{:x}", self.0)
344    }
345}
346
347impl From<u64> for TrainingSessionID {
348    fn from(id: u64) -> Self {
349        TrainingSessionID(id)
350    }
351}
352
353impl From<TrainingSessionID> for u64 {
354    fn from(val: TrainingSessionID) -> Self {
355        val.0
356    }
357}
358
359impl TrainingSessionID {
360    pub fn value(&self) -> u64 {
361        self.0
362    }
363}
364
365impl TryFrom<&str> for TrainingSessionID {
366    type Error = Error;
367
368    fn try_from(s: &str) -> Result<Self, Self::Error> {
369        TrainingSessionID::from_str(s)
370    }
371}
372
373impl TryFrom<String> for TrainingSessionID {
374    type Error = Error;
375
376    fn try_from(s: String) -> Result<Self, Self::Error> {
377        TrainingSessionID::from_str(&s)
378    }
379}
380
381impl FromStr for TrainingSessionID {
382    type Err = Error;
383
384    fn from_str(s: &str) -> Result<Self, Self::Err> {
385        let hex_part = s.strip_prefix("t-").ok_or_else(|| {
386            Error::InvalidParameters("Training Session ID must start with 't-' prefix".to_string())
387        })?;
388        let id = u64::from_str_radix(hex_part, 16)?;
389        Ok(TrainingSessionID(id))
390    }
391}
392
393/// Unique identifier for a validation session within an experiment.
394///
395/// Validation sessions represent model validation runs that evaluate trained
396/// models against test datasets. Each validation session has a unique ID
397/// displayed in hexadecimal format with a "v-" prefix (e.g., "v-345678").
398///
399/// # Examples
400///
401/// ```rust
402/// use edgefirst_client::ValidationSessionID;
403///
404/// // Create from u64
405/// let validation_id = ValidationSessionID::from(3456789);
406/// println!("{}", validation_id); // Displays: v-34c985
407///
408/// // Parse from string
409/// let validation_id: ValidationSessionID = "v-deadbeef".try_into().unwrap();
410/// assert_eq!(validation_id.value(), 0xdeadbeef);
411/// ```
412#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
413pub struct ValidationSessionID(u64);
414
415impl Display for ValidationSessionID {
416    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
417        write!(f, "v-{:x}", self.0)
418    }
419}
420
421impl From<u64> for ValidationSessionID {
422    fn from(id: u64) -> Self {
423        ValidationSessionID(id)
424    }
425}
426
427impl From<ValidationSessionID> for u64 {
428    fn from(val: ValidationSessionID) -> Self {
429        val.0
430    }
431}
432
433impl ValidationSessionID {
434    pub fn value(&self) -> u64 {
435        self.0
436    }
437}
438
439impl TryFrom<&str> for ValidationSessionID {
440    type Error = Error;
441
442    fn try_from(s: &str) -> Result<Self, Self::Error> {
443        let hex_part = s.strip_prefix("v-").ok_or_else(|| {
444            Error::InvalidParameters(
445                "Validation Session ID must start with 'v-' prefix".to_string(),
446            )
447        })?;
448        let id = u64::from_str_radix(hex_part, 16)?;
449        Ok(ValidationSessionID(id))
450    }
451}
452
453impl TryFrom<String> for ValidationSessionID {
454    type Error = Error;
455
456    fn try_from(s: String) -> Result<Self, Self::Error> {
457        ValidationSessionID::try_from(s.as_str())
458    }
459}
460
461#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
462pub struct SnapshotID(u64);
463
464impl Display for SnapshotID {
465    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
466        write!(f, "ss-{:x}", self.0)
467    }
468}
469
470impl From<u64> for SnapshotID {
471    fn from(id: u64) -> Self {
472        SnapshotID(id)
473    }
474}
475
476impl From<SnapshotID> for u64 {
477    fn from(val: SnapshotID) -> Self {
478        val.0
479    }
480}
481
482impl SnapshotID {
483    pub fn value(&self) -> u64 {
484        self.0
485    }
486}
487
488impl TryFrom<&str> for SnapshotID {
489    type Error = Error;
490
491    fn try_from(s: &str) -> Result<Self, Self::Error> {
492        let hex_part = s.strip_prefix("ss-").ok_or_else(|| {
493            Error::InvalidParameters("Snapshot ID must start with 'ss-' prefix".to_string())
494        })?;
495        let id = u64::from_str_radix(hex_part, 16)?;
496        Ok(SnapshotID(id))
497    }
498}
499
500#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
501pub struct TaskID(u64);
502
503impl Display for TaskID {
504    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
505        write!(f, "task-{:x}", self.0)
506    }
507}
508
509impl From<u64> for TaskID {
510    fn from(id: u64) -> Self {
511        TaskID(id)
512    }
513}
514
515impl From<TaskID> for u64 {
516    fn from(val: TaskID) -> Self {
517        val.0
518    }
519}
520
521impl TaskID {
522    pub fn value(&self) -> u64 {
523        self.0
524    }
525}
526
527impl TryFrom<&str> for TaskID {
528    type Error = Error;
529
530    fn try_from(s: &str) -> Result<Self, Self::Error> {
531        TaskID::from_str(s)
532    }
533}
534
535impl TryFrom<String> for TaskID {
536    type Error = Error;
537
538    fn try_from(s: String) -> Result<Self, Self::Error> {
539        TaskID::from_str(&s)
540    }
541}
542
543impl FromStr for TaskID {
544    type Err = Error;
545
546    fn from_str(s: &str) -> Result<Self, Self::Err> {
547        let hex_part = s.strip_prefix("task-").ok_or_else(|| {
548            Error::InvalidParameters("Task ID must start with 'task-' prefix".to_string())
549        })?;
550        let id = u64::from_str_radix(hex_part, 16)?;
551        Ok(TaskID(id))
552    }
553}
554
555/// Unique identifier for a dataset within a project.
556///
557/// Datasets contain collections of images, annotations, and other data used for
558/// machine learning experiments. Each dataset has a unique ID displayed in
559/// hexadecimal format with a "ds-" prefix (e.g., "ds-123abc").
560///
561/// # Examples
562///
563/// ```rust
564/// use edgefirst_client::DatasetID;
565/// use std::str::FromStr;
566///
567/// // Create from u64
568/// let dataset_id = DatasetID::from(1193046);
569/// println!("{}", dataset_id); // Displays: ds-123abc
570///
571/// // Parse from string
572/// let dataset_id = DatasetID::from_str("ds-456def").unwrap();
573/// assert_eq!(dataset_id.value(), 0x456def);
574/// ```
575#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
576pub struct DatasetID(u64);
577
578impl Display for DatasetID {
579    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
580        write!(f, "ds-{:x}", self.0)
581    }
582}
583
584impl From<u64> for DatasetID {
585    fn from(id: u64) -> Self {
586        DatasetID(id)
587    }
588}
589
590impl From<DatasetID> for u64 {
591    fn from(val: DatasetID) -> Self {
592        val.0
593    }
594}
595
596impl DatasetID {
597    pub fn value(&self) -> u64 {
598        self.0
599    }
600}
601
602impl TryFrom<&str> for DatasetID {
603    type Error = Error;
604
605    fn try_from(s: &str) -> Result<Self, Self::Error> {
606        DatasetID::from_str(s)
607    }
608}
609
610impl TryFrom<String> for DatasetID {
611    type Error = Error;
612
613    fn try_from(s: String) -> Result<Self, Self::Error> {
614        DatasetID::from_str(&s)
615    }
616}
617
618impl FromStr for DatasetID {
619    type Err = Error;
620
621    fn from_str(s: &str) -> Result<Self, Self::Err> {
622        let hex_part = s.strip_prefix("ds-").ok_or_else(|| {
623            Error::InvalidParameters("Dataset ID must start with 'ds-' prefix".to_string())
624        })?;
625        let id = u64::from_str_radix(hex_part, 16)?;
626        Ok(DatasetID(id))
627    }
628}
629
630#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
631pub struct AnnotationSetID(u64);
632
633impl Display for AnnotationSetID {
634    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
635        write!(f, "as-{:x}", self.0)
636    }
637}
638
639impl From<u64> for AnnotationSetID {
640    fn from(id: u64) -> Self {
641        AnnotationSetID(id)
642    }
643}
644
645impl From<AnnotationSetID> for u64 {
646    fn from(val: AnnotationSetID) -> Self {
647        val.0
648    }
649}
650
651impl AnnotationSetID {
652    pub fn value(&self) -> u64 {
653        self.0
654    }
655}
656
657impl TryFrom<&str> for AnnotationSetID {
658    type Error = Error;
659
660    fn try_from(s: &str) -> Result<Self, Self::Error> {
661        AnnotationSetID::from_str(s)
662    }
663}
664
665impl TryFrom<String> for AnnotationSetID {
666    type Error = Error;
667
668    fn try_from(s: String) -> Result<Self, Self::Error> {
669        AnnotationSetID::from_str(&s)
670    }
671}
672
673impl FromStr for AnnotationSetID {
674    type Err = Error;
675
676    fn from_str(s: &str) -> Result<Self, Self::Err> {
677        let hex_part = s.strip_prefix("as-").ok_or_else(|| {
678            Error::InvalidParameters("Annotation Set ID must start with 'as-' prefix".to_string())
679        })?;
680        let id = u64::from_str_radix(hex_part, 16)?;
681        Ok(AnnotationSetID(id))
682    }
683}
684
685#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
686pub struct SampleID(u64);
687
688impl Display for SampleID {
689    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
690        write!(f, "s-{:x}", self.0)
691    }
692}
693
694impl From<u64> for SampleID {
695    fn from(id: u64) -> Self {
696        SampleID(id)
697    }
698}
699
700impl From<SampleID> for u64 {
701    fn from(val: SampleID) -> Self {
702        val.0
703    }
704}
705
706impl SampleID {
707    pub fn value(&self) -> u64 {
708        self.0
709    }
710}
711
712impl TryFrom<&str> for SampleID {
713    type Error = Error;
714
715    fn try_from(s: &str) -> Result<Self, Self::Error> {
716        let hex_part = s.strip_prefix("s-").ok_or_else(|| {
717            Error::InvalidParameters("Sample ID must start with 's-' prefix".to_string())
718        })?;
719        let id = u64::from_str_radix(hex_part, 16)?;
720        Ok(SampleID(id))
721    }
722}
723
724#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
725pub struct AppId(u64);
726
727impl Display for AppId {
728    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
729        write!(f, "app-{:x}", self.0)
730    }
731}
732
733impl From<u64> for AppId {
734    fn from(id: u64) -> Self {
735        AppId(id)
736    }
737}
738
739impl From<AppId> for u64 {
740    fn from(val: AppId) -> Self {
741        val.0
742    }
743}
744
745impl AppId {
746    pub fn value(&self) -> u64 {
747        self.0
748    }
749}
750
751impl TryFrom<&str> for AppId {
752    type Error = Error;
753
754    fn try_from(s: &str) -> Result<Self, Self::Error> {
755        let hex_part = s.strip_prefix("app-").ok_or_else(|| {
756            Error::InvalidParameters("App ID must start with 'app-' prefix".to_string())
757        })?;
758        let id = u64::from_str_radix(hex_part, 16)?;
759        Ok(AppId(id))
760    }
761}
762
763#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
764pub struct ImageId(u64);
765
766impl Display for ImageId {
767    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
768        write!(f, "im-{:x}", self.0)
769    }
770}
771
772impl From<u64> for ImageId {
773    fn from(id: u64) -> Self {
774        ImageId(id)
775    }
776}
777
778impl From<ImageId> for u64 {
779    fn from(val: ImageId) -> Self {
780        val.0
781    }
782}
783
784impl ImageId {
785    pub fn value(&self) -> u64 {
786        self.0
787    }
788}
789
790impl TryFrom<&str> for ImageId {
791    type Error = Error;
792
793    fn try_from(s: &str) -> Result<Self, Self::Error> {
794        let hex_part = s.strip_prefix("im-").ok_or_else(|| {
795            Error::InvalidParameters("Image ID must start with 'im-' prefix".to_string())
796        })?;
797        let id = u64::from_str_radix(hex_part, 16)?;
798        Ok(ImageId(id))
799    }
800}
801
802#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
803pub struct SequenceId(u64);
804
805impl Display for SequenceId {
806    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
807        write!(f, "se-{:x}", self.0)
808    }
809}
810
811impl From<u64> for SequenceId {
812    fn from(id: u64) -> Self {
813        SequenceId(id)
814    }
815}
816
817impl From<SequenceId> for u64 {
818    fn from(val: SequenceId) -> Self {
819        val.0
820    }
821}
822
823impl SequenceId {
824    pub fn value(&self) -> u64 {
825        self.0
826    }
827}
828
829impl TryFrom<&str> for SequenceId {
830    type Error = Error;
831
832    fn try_from(s: &str) -> Result<Self, Self::Error> {
833        let hex_part = s.strip_prefix("se-").ok_or_else(|| {
834            Error::InvalidParameters("Sequence ID must start with 'se-' prefix".to_string())
835        })?;
836        let id = u64::from_str_radix(hex_part, 16)?;
837        Ok(SequenceId(id))
838    }
839}
840
841/// The project class represents a project in the EdgeFirst Studio.  A project
842/// contains datasets, experiments, and other resources related to a specific
843/// task or workflow.
844#[derive(Deserialize, Clone, Debug)]
845pub struct Project {
846    id: ProjectID,
847    name: String,
848    description: String,
849}
850
851impl Display for Project {
852    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
853        write!(f, "{} {}", self.id(), self.name())
854    }
855}
856
857impl Project {
858    pub fn id(&self) -> ProjectID {
859        self.id
860    }
861
862    pub fn name(&self) -> &str {
863        &self.name
864    }
865
866    pub fn description(&self) -> &str {
867        &self.description
868    }
869
870    pub async fn datasets(
871        &self,
872        client: &client::Client,
873        name: Option<&str>,
874    ) -> Result<Vec<Dataset>, Error> {
875        client.datasets(self.id, name).await
876    }
877
878    pub async fn experiments(
879        &self,
880        client: &client::Client,
881        name: Option<&str>,
882    ) -> Result<Vec<Experiment>, Error> {
883        client.experiments(self.id, name).await
884    }
885}
886
887#[derive(Deserialize, Debug)]
888pub struct SamplesCountResult {
889    pub total: u64,
890}
891
892#[derive(Serialize, Clone, Debug)]
893pub struct SamplesListParams {
894    pub dataset_id: DatasetID,
895    #[serde(skip_serializing_if = "Option::is_none")]
896    pub annotation_set_id: Option<AnnotationSetID>,
897    #[serde(skip_serializing_if = "Option::is_none")]
898    pub continue_token: Option<String>,
899    #[serde(skip_serializing_if = "Vec::is_empty")]
900    pub types: Vec<String>,
901    #[serde(skip_serializing_if = "Vec::is_empty")]
902    pub group_names: Vec<String>,
903}
904
905#[derive(Deserialize, Debug)]
906pub struct SamplesListResult {
907    pub samples: Vec<Sample>,
908    pub continue_token: Option<String>,
909}
910
911/// Parameters for populating (importing) samples into a dataset.
912///
913/// Used with the `samples.populate` API to create new samples in a dataset,
914/// optionally with annotations and sensor data files.
915#[derive(Serialize, Clone, Debug)]
916pub struct SamplesPopulateParams {
917    pub dataset_id: DatasetID,
918    #[serde(skip_serializing_if = "Option::is_none")]
919    pub annotation_set_id: Option<AnnotationSetID>,
920    #[serde(skip_serializing_if = "Option::is_none")]
921    pub presigned_urls: Option<bool>,
922    pub samples: Vec<Sample>,
923}
924
925/// Result from the `samples.populate` API call.
926///
927/// The API returns an array of populated sample results, one for each sample
928/// that was submitted. Each result contains the sample UUID and presigned URLs
929/// for uploading the associated files.
930#[derive(Deserialize, Debug)]
931pub struct SamplesPopulateResult {
932    /// UUID of the sample that was populated
933    pub uuid: String,
934    /// Presigned URLs for uploading files for this sample
935    pub urls: Vec<PresignedUrl>,
936}
937
938/// A presigned URL for uploading a file to S3.
939#[derive(Deserialize, Debug, Clone)]
940pub struct PresignedUrl {
941    /// Filename as specified in the sample
942    pub filename: String,
943    /// S3 key path
944    pub key: String,
945    /// Presigned URL for uploading (PUT request)
946    pub url: String,
947}
948
949#[derive(Deserialize)]
950pub struct Snapshot {
951    id: SnapshotID,
952    description: String,
953    status: String,
954    path: String,
955    #[serde(rename = "date")]
956    created: DateTime<Utc>,
957}
958
959impl Display for Snapshot {
960    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
961        write!(f, "{} {}", self.id, self.description)
962    }
963}
964
965impl Snapshot {
966    pub fn id(&self) -> SnapshotID {
967        self.id
968    }
969
970    pub fn description(&self) -> &str {
971        &self.description
972    }
973
974    pub fn status(&self) -> &str {
975        &self.status
976    }
977
978    pub fn path(&self) -> &str {
979        &self.path
980    }
981
982    pub fn created(&self) -> &DateTime<Utc> {
983        &self.created
984    }
985}
986
987#[derive(Serialize, Debug)]
988pub struct SnapshotRestore {
989    pub project_id: ProjectID,
990    pub snapshot_id: SnapshotID,
991    pub fps: u64,
992    #[serde(rename = "enabled_topics", skip_serializing_if = "Vec::is_empty")]
993    pub topics: Vec<String>,
994    #[serde(rename = "label_names", skip_serializing_if = "Vec::is_empty")]
995    pub autolabel: Vec<String>,
996    #[serde(rename = "depth_gen")]
997    pub autodepth: bool,
998    pub agtg_pipeline: bool,
999    #[serde(skip_serializing_if = "Option::is_none")]
1000    pub dataset_name: Option<String>,
1001    #[serde(skip_serializing_if = "Option::is_none")]
1002    pub dataset_description: Option<String>,
1003}
1004
1005#[derive(Deserialize, Debug)]
1006pub struct SnapshotRestoreResult {
1007    pub id: SnapshotID,
1008    pub description: String,
1009    pub dataset_name: String,
1010    pub dataset_id: DatasetID,
1011    pub annotation_set_id: AnnotationSetID,
1012    #[serde(rename = "docker_task_id")]
1013    pub task_id: TaskID,
1014    pub date: DateTime<Utc>,
1015}
1016
1017#[derive(Deserialize)]
1018pub struct Experiment {
1019    id: ExperimentID,
1020    project_id: ProjectID,
1021    name: String,
1022    description: String,
1023}
1024
1025impl Display for Experiment {
1026    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1027        write!(f, "{} {}", self.uid(), self.name)
1028    }
1029}
1030
1031impl Experiment {
1032    pub fn id(&self) -> ExperimentID {
1033        self.id
1034    }
1035
1036    pub fn uid(&self) -> String {
1037        self.id.to_string()
1038    }
1039
1040    pub fn project_id(&self) -> ProjectID {
1041        self.project_id
1042    }
1043
1044    pub fn name(&self) -> &str {
1045        &self.name
1046    }
1047
1048    pub fn description(&self) -> &str {
1049        &self.description
1050    }
1051
1052    pub async fn project(&self, client: &client::Client) -> Result<Project, Error> {
1053        client.project(self.project_id).await
1054    }
1055
1056    pub async fn training_sessions(
1057        &self,
1058        client: &client::Client,
1059        name: Option<&str>,
1060    ) -> Result<Vec<TrainingSession>, Error> {
1061        client.training_sessions(self.id, name).await
1062    }
1063}
1064
1065#[derive(Serialize, Debug)]
1066pub struct PublishMetrics {
1067    #[serde(rename = "trainer_session_id", skip_serializing_if = "Option::is_none")]
1068    pub trainer_session_id: Option<TrainingSessionID>,
1069    #[serde(
1070        rename = "validate_session_id",
1071        skip_serializing_if = "Option::is_none"
1072    )]
1073    pub validate_session_id: Option<ValidationSessionID>,
1074    pub metrics: HashMap<String, Parameter>,
1075}
1076
1077#[derive(Deserialize)]
1078struct TrainingSessionParams {
1079    model_params: HashMap<String, Parameter>,
1080    dataset_params: DatasetParams,
1081}
1082
1083#[derive(Deserialize)]
1084pub struct TrainingSession {
1085    id: TrainingSessionID,
1086    #[serde(rename = "trainer_id")]
1087    experiment_id: ExperimentID,
1088    model: String,
1089    name: String,
1090    description: String,
1091    params: TrainingSessionParams,
1092    #[serde(rename = "docker_task")]
1093    task: Task,
1094}
1095
1096impl Display for TrainingSession {
1097    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1098        write!(f, "{} {}", self.uid(), self.name())
1099    }
1100}
1101
1102impl TrainingSession {
1103    pub fn id(&self) -> TrainingSessionID {
1104        self.id
1105    }
1106
1107    pub fn uid(&self) -> String {
1108        self.id.to_string()
1109    }
1110
1111    pub fn name(&self) -> &str {
1112        &self.name
1113    }
1114
1115    pub fn description(&self) -> &str {
1116        &self.description
1117    }
1118
1119    pub fn model(&self) -> &str {
1120        &self.model
1121    }
1122
1123    pub fn experiment_id(&self) -> ExperimentID {
1124        self.experiment_id
1125    }
1126
1127    pub fn task(&self) -> Task {
1128        self.task.clone()
1129    }
1130
1131    pub fn model_params(&self) -> &HashMap<String, Parameter> {
1132        &self.params.model_params
1133    }
1134
1135    pub fn dataset_params(&self) -> &DatasetParams {
1136        &self.params.dataset_params
1137    }
1138
1139    pub fn train_group(&self) -> &str {
1140        &self.params.dataset_params.train_group
1141    }
1142
1143    pub fn val_group(&self) -> &str {
1144        &self.params.dataset_params.val_group
1145    }
1146
1147    pub async fn experiment(&self, client: &client::Client) -> Result<Experiment, Error> {
1148        client.experiment(self.experiment_id).await
1149    }
1150
1151    pub async fn dataset(&self, client: &client::Client) -> Result<Dataset, Error> {
1152        client.dataset(self.params.dataset_params.dataset_id).await
1153    }
1154
1155    pub async fn annotation_set(&self, client: &client::Client) -> Result<AnnotationSet, Error> {
1156        client
1157            .annotation_set(self.params.dataset_params.annotation_set_id)
1158            .await
1159    }
1160
1161    pub async fn artifacts(&self, client: &client::Client) -> Result<Vec<Artifact>, Error> {
1162        client.artifacts(self.id).await
1163    }
1164
1165    pub async fn metrics(
1166        &self,
1167        client: &client::Client,
1168    ) -> Result<HashMap<String, Parameter>, Error> {
1169        #[derive(Deserialize)]
1170        #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1171        enum Response {
1172            Empty {},
1173            Map(HashMap<String, Parameter>),
1174            String(String),
1175        }
1176
1177        let params = HashMap::from([("trainer_session_id", self.id().value())]);
1178        let resp: Response = client
1179            .rpc("trainer.session.metrics".to_owned(), Some(params))
1180            .await?;
1181
1182        Ok(match resp {
1183            Response::String(metrics) => serde_json::from_str(&metrics)?,
1184            Response::Map(metrics) => metrics,
1185            Response::Empty {} => HashMap::new(),
1186        })
1187    }
1188
1189    pub async fn set_metrics(
1190        &self,
1191        client: &client::Client,
1192        metrics: HashMap<String, Parameter>,
1193    ) -> Result<(), Error> {
1194        let metrics = PublishMetrics {
1195            trainer_session_id: Some(self.id()),
1196            validate_session_id: None,
1197            metrics,
1198        };
1199
1200        let _: String = client
1201            .rpc("trainer.session.metrics".to_owned(), Some(metrics))
1202            .await?;
1203
1204        Ok(())
1205    }
1206
1207    /// Downloads an artifact from the training session.
1208    pub async fn download_artifact(
1209        &self,
1210        client: &client::Client,
1211        filename: &str,
1212    ) -> Result<Vec<u8>, Error> {
1213        client
1214            .fetch(&format!(
1215                "download_model?training_session_id={}&file={}",
1216                self.id().value(),
1217                filename
1218            ))
1219            .await
1220    }
1221
1222    /// Uploads an artifact to the training session.  The filename will
1223    /// be used as the name of the file in the training session while path is
1224    /// the local path to the file to upload.
1225    pub async fn upload_artifact(
1226        &self,
1227        client: &client::Client,
1228        filename: &str,
1229        path: PathBuf,
1230    ) -> Result<(), Error> {
1231        self.upload(client, &[(format!("artifacts/{}", filename), path)])
1232            .await
1233    }
1234
1235    /// Downloads a checkpoint file from the training session.
1236    pub async fn download_checkpoint(
1237        &self,
1238        client: &client::Client,
1239        filename: &str,
1240    ) -> Result<Vec<u8>, Error> {
1241        client
1242            .fetch(&format!(
1243                "download_checkpoint?folder=checkpoints&training_session_id={}&file={}",
1244                self.id().value(),
1245                filename
1246            ))
1247            .await
1248    }
1249
1250    /// Uploads a checkpoint file to the training session.  The filename will
1251    /// be used as the name of the file in the training session while path is
1252    /// the local path to the file to upload.
1253    pub async fn upload_checkpoint(
1254        &self,
1255        client: &client::Client,
1256        filename: &str,
1257        path: PathBuf,
1258    ) -> Result<(), Error> {
1259        self.upload(client, &[(format!("checkpoints/{}", filename), path)])
1260            .await
1261    }
1262
1263    /// Downloads a file from the training session.  Should only be used for
1264    /// text files, binary files must be downloaded using download_artifact or
1265    /// download_checkpoint.
1266    pub async fn download(&self, client: &client::Client, filename: &str) -> Result<String, Error> {
1267        #[derive(Serialize)]
1268        struct DownloadRequest {
1269            session_id: TrainingSessionID,
1270            file_path: String,
1271        }
1272
1273        let params = DownloadRequest {
1274            session_id: self.id(),
1275            file_path: filename.to_string(),
1276        };
1277
1278        client
1279            .rpc("trainer.download.file".to_owned(), Some(params))
1280            .await
1281    }
1282
1283    pub async fn upload(
1284        &self,
1285        client: &client::Client,
1286        files: &[(String, PathBuf)],
1287    ) -> Result<(), Error> {
1288        let mut parts = Form::new().part(
1289            "params",
1290            Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1291        );
1292
1293        for (name, path) in files {
1294            let file_part = Part::file(path).await?.file_name(name.to_owned());
1295            parts = parts.part("file", file_part);
1296        }
1297
1298        let result = client.post_multipart("trainer.upload.files", parts).await?;
1299        trace!("TrainingSession::upload: {:?}", result);
1300        Ok(())
1301    }
1302}
1303
1304#[derive(Deserialize, Clone, Debug)]
1305pub struct ValidationSession {
1306    id: ValidationSessionID,
1307    description: String,
1308    dataset_id: DatasetID,
1309    experiment_id: ExperimentID,
1310    training_session_id: TrainingSessionID,
1311    #[serde(rename = "gt_annotation_set_id")]
1312    annotation_set_id: AnnotationSetID,
1313    #[serde(deserialize_with = "validation_session_params")]
1314    params: HashMap<String, Parameter>,
1315    #[serde(rename = "docker_task")]
1316    task: Task,
1317}
1318
1319fn validation_session_params<'de, D>(
1320    deserializer: D,
1321) -> Result<HashMap<String, Parameter>, D::Error>
1322where
1323    D: Deserializer<'de>,
1324{
1325    #[derive(Deserialize)]
1326    struct ModelParams {
1327        validation: Option<HashMap<String, Parameter>>,
1328    }
1329
1330    #[derive(Deserialize)]
1331    struct ValidateParams {
1332        model: String,
1333    }
1334
1335    #[derive(Deserialize)]
1336    struct Params {
1337        model_params: ModelParams,
1338        validate_params: ValidateParams,
1339    }
1340
1341    let params = Params::deserialize(deserializer)?;
1342    let params = match params.model_params.validation {
1343        Some(mut map) => {
1344            map.insert(
1345                "model".to_string(),
1346                Parameter::String(params.validate_params.model),
1347            );
1348            map
1349        }
1350        None => HashMap::from([(
1351            "model".to_string(),
1352            Parameter::String(params.validate_params.model),
1353        )]),
1354    };
1355
1356    Ok(params)
1357}
1358
1359impl ValidationSession {
1360    pub fn id(&self) -> ValidationSessionID {
1361        self.id
1362    }
1363
1364    pub fn uid(&self) -> String {
1365        self.id.to_string()
1366    }
1367
1368    pub fn name(&self) -> &str {
1369        self.task.name()
1370    }
1371
1372    pub fn description(&self) -> &str {
1373        &self.description
1374    }
1375
1376    pub fn dataset_id(&self) -> DatasetID {
1377        self.dataset_id
1378    }
1379
1380    pub fn experiment_id(&self) -> ExperimentID {
1381        self.experiment_id
1382    }
1383
1384    pub fn training_session_id(&self) -> TrainingSessionID {
1385        self.training_session_id
1386    }
1387
1388    pub fn annotation_set_id(&self) -> AnnotationSetID {
1389        self.annotation_set_id
1390    }
1391
1392    pub fn params(&self) -> &HashMap<String, Parameter> {
1393        &self.params
1394    }
1395
1396    pub fn task(&self) -> &Task {
1397        &self.task
1398    }
1399
1400    pub async fn metrics(
1401        &self,
1402        client: &client::Client,
1403    ) -> Result<HashMap<String, Parameter>, Error> {
1404        #[derive(Deserialize)]
1405        #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1406        enum Response {
1407            Empty {},
1408            Map(HashMap<String, Parameter>),
1409            String(String),
1410        }
1411
1412        let params = HashMap::from([("validate_session_id", self.id().value())]);
1413        let resp: Response = client
1414            .rpc("validate.session.metrics".to_owned(), Some(params))
1415            .await?;
1416
1417        Ok(match resp {
1418            Response::String(metrics) => serde_json::from_str(&metrics)?,
1419            Response::Map(metrics) => metrics,
1420            Response::Empty {} => HashMap::new(),
1421        })
1422    }
1423
1424    pub async fn set_metrics(
1425        &self,
1426        client: &client::Client,
1427        metrics: HashMap<String, Parameter>,
1428    ) -> Result<(), Error> {
1429        let metrics = PublishMetrics {
1430            trainer_session_id: None,
1431            validate_session_id: Some(self.id()),
1432            metrics,
1433        };
1434
1435        let _: String = client
1436            .rpc("validate.session.metrics".to_owned(), Some(metrics))
1437            .await?;
1438
1439        Ok(())
1440    }
1441
1442    pub async fn upload(
1443        &self,
1444        client: &client::Client,
1445        files: &[(String, PathBuf)],
1446    ) -> Result<(), Error> {
1447        let mut parts = Form::new().part(
1448            "params",
1449            Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1450        );
1451
1452        for (name, path) in files {
1453            let file_part = Part::file(path).await?.file_name(name.to_owned());
1454            parts = parts.part("file", file_part);
1455        }
1456
1457        let result = client
1458            .post_multipart("validate.upload.files", parts)
1459            .await?;
1460        trace!("ValidationSession::upload: {:?}", result);
1461        Ok(())
1462    }
1463}
1464
1465#[derive(Deserialize, Clone, Debug)]
1466pub struct DatasetParams {
1467    dataset_id: DatasetID,
1468    annotation_set_id: AnnotationSetID,
1469    #[serde(rename = "train_group_name")]
1470    train_group: String,
1471    #[serde(rename = "val_group_name")]
1472    val_group: String,
1473}
1474
1475impl DatasetParams {
1476    pub fn dataset_id(&self) -> DatasetID {
1477        self.dataset_id
1478    }
1479
1480    pub fn annotation_set_id(&self) -> AnnotationSetID {
1481        self.annotation_set_id
1482    }
1483
1484    pub fn train_group(&self) -> &str {
1485        &self.train_group
1486    }
1487
1488    pub fn val_group(&self) -> &str {
1489        &self.val_group
1490    }
1491}
1492
1493#[derive(Serialize, Debug, Clone)]
1494pub struct TasksListParams {
1495    #[serde(skip_serializing_if = "Option::is_none")]
1496    pub continue_token: Option<String>,
1497    #[serde(rename = "manage_types", skip_serializing_if = "Option::is_none")]
1498    pub manager: Option<Vec<String>>,
1499    #[serde(skip_serializing_if = "Option::is_none")]
1500    pub status: Option<Vec<String>>,
1501}
1502
1503#[derive(Deserialize, Debug, Clone)]
1504pub struct TasksListResult {
1505    pub tasks: Vec<Task>,
1506    pub continue_token: Option<String>,
1507}
1508
1509#[derive(Deserialize, Debug, Clone)]
1510pub struct Task {
1511    id: TaskID,
1512    name: String,
1513    #[serde(rename = "type")]
1514    workflow: String,
1515    status: String,
1516    #[serde(rename = "manage_type")]
1517    manager: Option<String>,
1518    #[serde(rename = "instance_type")]
1519    instance: String,
1520    #[serde(rename = "date")]
1521    created: DateTime<Utc>,
1522}
1523
1524impl Task {
1525    pub fn id(&self) -> TaskID {
1526        self.id
1527    }
1528
1529    pub fn uid(&self) -> String {
1530        self.id.to_string()
1531    }
1532
1533    pub fn name(&self) -> &str {
1534        &self.name
1535    }
1536
1537    pub fn workflow(&self) -> &str {
1538        &self.workflow
1539    }
1540
1541    pub fn status(&self) -> &str {
1542        &self.status
1543    }
1544
1545    pub fn manager(&self) -> Option<&str> {
1546        self.manager.as_deref()
1547    }
1548
1549    pub fn instance(&self) -> &str {
1550        &self.instance
1551    }
1552
1553    pub fn created(&self) -> &DateTime<Utc> {
1554        &self.created
1555    }
1556}
1557
1558impl Display for Task {
1559    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1560        write!(
1561            f,
1562            "{} [{:?} {}] {}",
1563            self.uid(),
1564            self.manager(),
1565            self.workflow(),
1566            self.name()
1567        )
1568    }
1569}
1570
1571#[derive(Deserialize, Debug)]
1572pub struct TaskInfo {
1573    id: TaskID,
1574    project_id: Option<ProjectID>,
1575    #[serde(rename = "task_description")]
1576    description: String,
1577    #[serde(rename = "type")]
1578    workflow: String,
1579    status: Option<String>,
1580    progress: TaskProgress,
1581    #[serde(rename = "created_date")]
1582    created: DateTime<Utc>,
1583    #[serde(rename = "end_date")]
1584    completed: DateTime<Utc>,
1585}
1586
1587impl Display for TaskInfo {
1588    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1589        write!(
1590            f,
1591            "{} {}: {}",
1592            self.uid(),
1593            self.workflow(),
1594            self.description()
1595        )
1596    }
1597}
1598
1599impl TaskInfo {
1600    pub fn id(&self) -> TaskID {
1601        self.id
1602    }
1603
1604    pub fn uid(&self) -> String {
1605        self.id.to_string()
1606    }
1607
1608    pub fn project_id(&self) -> Option<ProjectID> {
1609        self.project_id
1610    }
1611
1612    pub fn description(&self) -> &str {
1613        &self.description
1614    }
1615
1616    pub fn workflow(&self) -> &str {
1617        &self.workflow
1618    }
1619
1620    pub fn status(&self) -> &Option<String> {
1621        &self.status
1622    }
1623
1624    pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1625        let t = client.task_status(self.id(), status).await?;
1626        self.status = Some(t.status);
1627        Ok(())
1628    }
1629
1630    pub fn stages(&self) -> HashMap<String, Stage> {
1631        match &self.progress.stages {
1632            Some(stages) => stages.clone(),
1633            None => HashMap::new(),
1634        }
1635    }
1636
1637    pub async fn update_stage(
1638        &mut self,
1639        client: &Client,
1640        stage: &str,
1641        status: &str,
1642        message: &str,
1643        percentage: u8,
1644    ) -> Result<(), Error> {
1645        client
1646            .update_stage(self.id(), stage, status, message, percentage)
1647            .await?;
1648        let t = client.task_info(self.id()).await?;
1649        self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1650        Ok(())
1651    }
1652
1653    pub async fn set_stages(
1654        &mut self,
1655        client: &Client,
1656        stages: &[(&str, &str)],
1657    ) -> Result<(), Error> {
1658        client.set_stages(self.id(), stages).await?;
1659        let t = client.task_info(self.id()).await?;
1660        self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1661        Ok(())
1662    }
1663
1664    pub fn created(&self) -> &DateTime<Utc> {
1665        &self.created
1666    }
1667
1668    pub fn completed(&self) -> &DateTime<Utc> {
1669        &self.completed
1670    }
1671}
1672
1673#[derive(Deserialize, Debug)]
1674pub struct TaskProgress {
1675    stages: Option<HashMap<String, Stage>>,
1676}
1677
1678#[derive(Serialize, Debug, Clone)]
1679pub struct TaskStatus {
1680    #[serde(rename = "docker_task_id")]
1681    pub task_id: TaskID,
1682    pub status: String,
1683}
1684
1685#[derive(Serialize, Deserialize, Debug, Clone)]
1686pub struct Stage {
1687    #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
1688    task_id: Option<TaskID>,
1689    stage: String,
1690    #[serde(skip_serializing_if = "Option::is_none")]
1691    status: Option<String>,
1692    #[serde(skip_serializing_if = "Option::is_none")]
1693    description: Option<String>,
1694    #[serde(skip_serializing_if = "Option::is_none")]
1695    message: Option<String>,
1696    percentage: u8,
1697}
1698
1699impl Stage {
1700    pub fn new(
1701        task_id: Option<TaskID>,
1702        stage: String,
1703        status: Option<String>,
1704        message: Option<String>,
1705        percentage: u8,
1706    ) -> Self {
1707        Stage {
1708            task_id,
1709            stage,
1710            status,
1711            description: None,
1712            message,
1713            percentage,
1714        }
1715    }
1716
1717    pub fn task_id(&self) -> &Option<TaskID> {
1718        &self.task_id
1719    }
1720
1721    pub fn stage(&self) -> &str {
1722        &self.stage
1723    }
1724
1725    pub fn status(&self) -> &Option<String> {
1726        &self.status
1727    }
1728
1729    pub fn description(&self) -> &Option<String> {
1730        &self.description
1731    }
1732
1733    pub fn message(&self) -> &Option<String> {
1734        &self.message
1735    }
1736
1737    pub fn percentage(&self) -> u8 {
1738        self.percentage
1739    }
1740}
1741
1742#[derive(Serialize, Debug)]
1743pub struct TaskStages {
1744    #[serde(rename = "docker_task_id")]
1745    pub task_id: TaskID,
1746    #[serde(skip_serializing_if = "Vec::is_empty")]
1747    pub stages: Vec<HashMap<String, String>>,
1748}
1749
1750#[derive(Deserialize, Debug)]
1751pub struct Artifact {
1752    name: String,
1753    #[serde(rename = "modelType")]
1754    model_type: String,
1755}
1756
1757impl Artifact {
1758    pub fn name(&self) -> &str {
1759        &self.name
1760    }
1761
1762    pub fn model_type(&self) -> &str {
1763        &self.model_type
1764    }
1765}