1use crate::{AnnotationSet, Client, Dataset, Error, Progress, 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
11fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
16where
17 D: Deserializer<'de>,
18 T: Default + Deserialize<'de>,
19{
20 Ok(Option::deserialize(deserializer)?.unwrap_or_default())
21}
22
23#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
53#[serde(untagged)]
54pub enum Parameter {
55 Integer(i64),
57 Real(f64),
59 Boolean(bool),
61 String(String),
63 Array(Vec<Parameter>),
65 Object(HashMap<String, Parameter>),
67}
68
69#[derive(Deserialize)]
70pub struct LoginResult {
71 pub(crate) token: String,
72}
73
74macro_rules! typeid {
83 ($(#[$meta:meta])* $name:ident, $prefix:literal) => {
84 $(#[$meta])*
85 #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
86 pub struct $name(u64);
87
88 impl Display for $name {
89 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
90 write!(f, concat!($prefix, "-{:x}"), self.0)
91 }
92 }
93
94 impl From<u64> for $name {
95 fn from(id: u64) -> Self {
96 $name(id)
97 }
98 }
99
100 impl From<$name> for u64 {
101 fn from(val: $name) -> Self {
102 val.0
103 }
104 }
105
106 impl $name {
107 pub fn value(&self) -> u64 {
109 self.0
110 }
111 }
112
113 impl TryFrom<&str> for $name {
114 type Error = Error;
115
116 fn try_from(s: &str) -> Result<Self, Self::Error> {
117 $name::from_str(s)
118 }
119 }
120
121 impl TryFrom<String> for $name {
122 type Error = Error;
123
124 fn try_from(s: String) -> Result<Self, Self::Error> {
125 $name::from_str(&s)
126 }
127 }
128
129 impl FromStr for $name {
130 type Err = Error;
131
132 fn from_str(s: &str) -> Result<Self, Self::Err> {
133 let hex_part =
134 s.strip_prefix(concat!($prefix, "-")).ok_or_else(|| {
135 Error::InvalidParameters(format!(
136 "{} must start with '{}-' prefix",
137 stringify!($name),
138 $prefix
139 ))
140 })?;
141 let id = u64::from_str_radix(hex_part, 16)?;
142 Ok($name(id))
143 }
144 }
145 };
146}
147
148typeid!(
149 OrganizationID,
169 "org"
170);
171
172#[derive(Deserialize, Clone, Debug)]
193pub struct Organization {
194 id: OrganizationID,
195 name: String,
196 #[serde(rename = "latest_credit")]
197 credits: i64,
198}
199
200impl Display for Organization {
201 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
202 write!(f, "{}", self.name())
203 }
204}
205
206impl Organization {
207 pub fn id(&self) -> OrganizationID {
208 self.id
209 }
210
211 pub fn name(&self) -> &str {
212 &self.name
213 }
214
215 pub fn credits(&self) -> i64 {
216 self.credits
217 }
218}
219
220#[derive(Deserialize, Clone, Debug)]
226pub struct UsageSummary {
227 #[serde(default)]
228 credits: f64,
229 #[serde(default)]
230 funds: f64,
231 #[serde(default, rename = "total_funds_and_credits")]
232 total: f64,
233}
234
235impl UsageSummary {
236 pub fn credits(&self) -> f64 {
237 self.credits
238 }
239
240 pub fn funds(&self) -> f64 {
241 self.funds
242 }
243
244 pub fn total(&self) -> f64 {
245 self.total
246 }
247}
248
249typeid!(
250 ProjectID,
271 "p"
272);
273
274typeid!(
275 ExperimentID,
296 "exp"
297);
298
299typeid!(
300 TrainingSessionID,
321 "t"
322);
323
324typeid!(
325 ValidationSessionID,
345 "v"
346);
347
348typeid!(
349 SnapshotID,
365 "ss"
366);
367
368typeid!(
369 TaskID,
385 "task"
386);
387
388typeid!(
389 DatasetID,
410 "ds"
411);
412
413typeid!(
414 AnnotationSetID,
430 "as"
431);
432
433typeid!(
434 SampleID,
450 "s"
451);
452
453typeid!(
454 AppId,
460 "app"
461);
462
463typeid!(
464 ImageId,
470 "im"
471);
472
473typeid!(
474 SequenceId,
480 "se"
481);
482
483#[derive(Deserialize, Clone, Debug)]
487pub struct Project {
488 id: ProjectID,
489 name: String,
490 description: String,
491}
492
493impl Display for Project {
494 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
495 write!(f, "{} {}", self.id(), self.name())
496 }
497}
498
499impl Project {
500 pub fn id(&self) -> ProjectID {
501 self.id
502 }
503
504 pub fn name(&self) -> &str {
505 &self.name
506 }
507
508 pub fn description(&self) -> &str {
509 &self.description
510 }
511
512 pub async fn datasets(
513 &self,
514 client: &client::Client,
515 name: Option<&str>,
516 ) -> Result<Vec<Dataset>, Error> {
517 client.datasets(self.id, name).await
518 }
519
520 pub async fn experiments(
521 &self,
522 client: &client::Client,
523 name: Option<&str>,
524 ) -> Result<Vec<Experiment>, Error> {
525 client.experiments(self.id, name).await
526 }
527}
528
529#[derive(Deserialize, Debug)]
530pub struct SamplesCountResult {
531 pub total: u64,
532}
533
534#[derive(Serialize, Clone, Debug)]
535pub struct SamplesListParams {
536 pub dataset_id: DatasetID,
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub annotation_set_id: Option<AnnotationSetID>,
539 #[serde(skip_serializing_if = "Option::is_none")]
540 pub continue_token: Option<String>,
541 #[serde(skip_serializing_if = "Vec::is_empty")]
542 pub types: Vec<String>,
543 #[serde(skip_serializing_if = "Vec::is_empty")]
544 pub group_names: Vec<String>,
545 #[serde(skip_serializing_if = "Option::is_none")]
546 pub tag: Option<String>,
547}
548
549#[derive(Deserialize, Debug)]
550pub struct SamplesListResult {
551 pub samples: Vec<Sample>,
552 pub continue_token: Option<String>,
553}
554
555#[derive(Serialize, Clone, Debug)]
557pub struct SampleDimensionUpdate {
558 pub id: SampleID,
559 pub width: u32,
560 pub height: u32,
561}
562
563#[derive(Serialize, Clone, Debug)]
565pub struct SamplesUpdateDimensionsParams {
566 pub dataset_id: DatasetID,
567 pub samples: Vec<SampleDimensionUpdate>,
568}
569
570#[derive(Deserialize, Debug)]
572pub struct SamplesUpdateDimensionsResult {
573 pub updated: u64,
574}
575
576#[derive(Serialize, Clone, Debug)]
581pub struct SamplesPopulateParams {
582 pub dataset_id: DatasetID,
583 #[serde(skip_serializing_if = "Option::is_none")]
584 pub annotation_set_id: Option<AnnotationSetID>,
585 #[serde(skip_serializing_if = "Option::is_none")]
586 pub presigned_urls: Option<bool>,
587 pub samples: Vec<Sample>,
588}
589
590#[derive(Deserialize, Debug, Clone)]
596pub struct SamplesPopulateResult {
597 pub uuid: String,
599 pub urls: Vec<PresignedUrl>,
601}
602
603#[derive(Deserialize, Debug, Clone)]
605pub struct PresignedUrl {
606 pub filename: String,
608 pub key: String,
610 pub url: String,
612}
613
614#[derive(Serialize, Clone, Debug)]
627pub struct ServerAnnotation {
628 #[serde(skip_serializing_if = "Option::is_none")]
630 pub label_id: Option<u64>,
631 #[serde(skip_serializing_if = "Option::is_none")]
633 pub label_index: Option<u64>,
634 #[serde(skip_serializing_if = "Option::is_none")]
636 pub label_name: Option<String>,
637 #[serde(rename = "type")]
639 pub annotation_type: String,
640 pub x: f64,
642 pub y: f64,
644 pub w: f64,
646 pub h: f64,
648 pub score: f64,
650 #[serde(skip_serializing_if = "String::is_empty")]
652 pub polygon: String,
653 pub image_id: u64,
655 pub annotation_set_id: u64,
657 #[serde(skip_serializing_if = "Option::is_none")]
659 pub object_reference: Option<String>,
660}
661
662#[derive(Serialize, Debug)]
664pub struct AnnotationAddBulkParams {
665 pub annotation_set_id: u64,
666 pub annotations: Vec<ServerAnnotation>,
667}
668
669#[derive(Serialize, Debug)]
671pub struct AnnotationBulkDeleteParams {
672 pub annotation_set_id: u64,
673 pub annotation_types: Vec<String>,
674 #[serde(skip_serializing_if = "Vec::is_empty")]
676 pub image_ids: Vec<u64>,
677 #[serde(skip_serializing_if = "Option::is_none")]
679 pub delete_all: Option<bool>,
680}
681
682#[derive(Deserialize)]
683pub struct Snapshot {
684 id: SnapshotID,
685 description: String,
686 status: String,
687 path: String,
688 #[serde(rename = "date")]
689 created: DateTime<Utc>,
690}
691
692impl Display for Snapshot {
693 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
694 write!(f, "{} {}", self.id, self.description)
695 }
696}
697
698impl Snapshot {
699 pub fn id(&self) -> SnapshotID {
700 self.id
701 }
702
703 pub fn description(&self) -> &str {
704 &self.description
705 }
706
707 pub fn status(&self) -> &str {
708 &self.status
709 }
710
711 pub fn path(&self) -> &str {
712 &self.path
713 }
714
715 pub fn created(&self) -> &DateTime<Utc> {
716 &self.created
717 }
718}
719
720#[derive(Serialize, Debug)]
721pub struct SnapshotRestore {
722 pub project_id: ProjectID,
723 pub snapshot_id: SnapshotID,
724 pub fps: u64,
725 #[serde(rename = "enabled_topics", skip_serializing_if = "Vec::is_empty")]
726 pub topics: Vec<String>,
727 #[serde(rename = "label_names", skip_serializing_if = "Vec::is_empty")]
728 pub autolabel: Vec<String>,
729 #[serde(rename = "depth_gen")]
730 pub autodepth: bool,
731 pub agtg_pipeline: bool,
732 #[serde(skip_serializing_if = "Option::is_none")]
733 pub dataset_name: Option<String>,
734 #[serde(skip_serializing_if = "Option::is_none")]
735 pub dataset_description: Option<String>,
736}
737
738#[derive(Deserialize, Debug)]
739pub struct SnapshotRestoreResult {
740 pub id: SnapshotID,
741 pub description: String,
742 pub dataset_name: String,
743 pub dataset_id: DatasetID,
744 pub annotation_set_id: AnnotationSetID,
745 #[serde(default)]
746 pub task_id: Option<TaskID>,
747 #[serde(default)]
751 pub date: Option<DateTime<Utc>>,
752}
753
754#[derive(Serialize, Debug)]
759pub struct SnapshotCreateFromDataset {
760 pub description: String,
762 pub dataset_id: DatasetID,
764 pub annotation_set_id: AnnotationSetID,
766}
767
768#[derive(Deserialize, Debug)]
772pub struct SnapshotFromDatasetResult {
773 #[serde(alias = "snapshot_id")]
775 pub id: SnapshotID,
776 #[serde(default)]
778 pub task_id: Option<TaskID>,
779}
780
781#[derive(Deserialize)]
782pub struct Experiment {
783 id: ExperimentID,
784 project_id: ProjectID,
785 name: String,
786 description: String,
787}
788
789impl Display for Experiment {
790 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
791 write!(f, "{} {}", self.id, self.name)
792 }
793}
794
795impl Experiment {
796 pub fn id(&self) -> ExperimentID {
797 self.id
798 }
799
800 pub fn project_id(&self) -> ProjectID {
801 self.project_id
802 }
803
804 pub fn name(&self) -> &str {
805 &self.name
806 }
807
808 pub fn description(&self) -> &str {
809 &self.description
810 }
811
812 pub async fn project(&self, client: &client::Client) -> Result<Project, Error> {
813 client.project(self.project_id).await
814 }
815
816 pub async fn training_sessions(
817 &self,
818 client: &client::Client,
819 name: Option<&str>,
820 ) -> Result<Vec<TrainingSession>, Error> {
821 client.training_sessions(self.id, name).await
822 }
823}
824
825#[derive(Serialize, Debug)]
826pub struct PublishMetrics {
827 #[serde(rename = "trainer_session_id", skip_serializing_if = "Option::is_none")]
828 pub trainer_session_id: Option<TrainingSessionID>,
829 #[serde(
830 rename = "validate_session_id",
831 skip_serializing_if = "Option::is_none"
832 )]
833 pub validate_session_id: Option<ValidationSessionID>,
834 pub metrics: HashMap<String, Parameter>,
835}
836
837#[derive(Deserialize)]
838struct TrainingSessionParams {
839 model_params: HashMap<String, Parameter>,
840 dataset_params: DatasetParams,
841}
842
843#[derive(Deserialize)]
844pub struct TrainingSession {
845 id: TrainingSessionID,
846 #[serde(rename = "trainer_id")]
847 experiment_id: ExperimentID,
848 model: String,
849 name: String,
850 description: String,
851 params: TrainingSessionParams,
852 #[serde(rename = "docker_task")]
853 task: Task,
854}
855
856impl Display for TrainingSession {
857 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
858 write!(f, "{} {}", self.id, self.name())
859 }
860}
861
862impl TrainingSession {
863 pub fn id(&self) -> TrainingSessionID {
864 self.id
865 }
866
867 pub fn name(&self) -> &str {
868 &self.name
869 }
870
871 pub fn description(&self) -> &str {
872 &self.description
873 }
874
875 pub fn model(&self) -> &str {
876 &self.model
877 }
878
879 pub fn experiment_id(&self) -> ExperimentID {
880 self.experiment_id
881 }
882
883 pub fn task(&self) -> Task {
884 self.task.clone()
885 }
886
887 pub fn model_params(&self) -> &HashMap<String, Parameter> {
888 &self.params.model_params
889 }
890
891 pub fn dataset_params(&self) -> &DatasetParams {
892 &self.params.dataset_params
893 }
894
895 pub fn train_group(&self) -> &str {
896 &self.params.dataset_params.train_group
897 }
898
899 pub fn val_group(&self) -> &str {
900 &self.params.dataset_params.val_group
901 }
902
903 pub async fn experiment(&self, client: &client::Client) -> Result<Experiment, Error> {
904 client.experiment(self.experiment_id).await
905 }
906
907 pub async fn dataset(&self, client: &client::Client) -> Result<Dataset, Error> {
908 client.dataset(self.params.dataset_params.dataset_id).await
909 }
910
911 pub async fn annotation_set(&self, client: &client::Client) -> Result<AnnotationSet, Error> {
912 client
913 .annotation_set(self.params.dataset_params.annotation_set_id)
914 .await
915 }
916
917 pub async fn artifacts(&self, client: &client::Client) -> Result<Vec<Artifact>, Error> {
918 client.artifacts(self.id).await
919 }
920
921 pub async fn metrics(
922 &self,
923 client: &client::Client,
924 ) -> Result<HashMap<String, Parameter>, Error> {
925 #[derive(Deserialize)]
926 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
927 enum Response {
928 Empty {},
929 Map(HashMap<String, Parameter>),
930 String(String),
931 }
932
933 let params = HashMap::from([("trainer_session_id", self.id().value())]);
934 let resp: Response = client
935 .rpc("trainer.session.metrics".to_owned(), Some(params))
936 .await?;
937
938 Ok(match resp {
939 Response::String(metrics) => serde_json::from_str(&metrics)?,
940 Response::Map(metrics) => metrics,
941 Response::Empty {} => HashMap::new(),
942 })
943 }
944
945 pub async fn set_metrics(
946 &self,
947 client: &client::Client,
948 metrics: HashMap<String, Parameter>,
949 ) -> Result<(), Error> {
950 let metrics = PublishMetrics {
951 trainer_session_id: Some(self.id()),
952 validate_session_id: None,
953 metrics,
954 };
955
956 let _: String = client
957 .rpc("trainer.session.metrics".to_owned(), Some(metrics))
958 .await?;
959
960 Ok(())
961 }
962
963 pub async fn download_artifact(
965 &self,
966 client: &client::Client,
967 filename: &str,
968 ) -> Result<Vec<u8>, Error> {
969 client
970 .fetch(&format!(
971 "download_model?training_session_id={}&file={}",
972 self.id().value(),
973 filename
974 ))
975 .await
976 }
977
978 pub async fn upload_artifact(
982 &self,
983 client: &client::Client,
984 filename: &str,
985 path: PathBuf,
986 ) -> Result<(), Error> {
987 self.upload(client, &[(format!("artifacts/{}", filename), path)])
988 .await
989 }
990
991 pub async fn download_checkpoint(
993 &self,
994 client: &client::Client,
995 filename: &str,
996 ) -> Result<Vec<u8>, Error> {
997 client
998 .fetch(&format!(
999 "download_checkpoint?folder=checkpoints&training_session_id={}&file={}",
1000 self.id().value(),
1001 filename
1002 ))
1003 .await
1004 }
1005
1006 pub async fn upload_checkpoint(
1010 &self,
1011 client: &client::Client,
1012 filename: &str,
1013 path: PathBuf,
1014 ) -> Result<(), Error> {
1015 self.upload(client, &[(format!("checkpoints/{}", filename), path)])
1016 .await
1017 }
1018
1019 pub async fn download(&self, client: &client::Client, filename: &str) -> Result<String, Error> {
1023 #[derive(Serialize)]
1024 struct DownloadRequest {
1025 session_id: TrainingSessionID,
1026 file_path: String,
1027 }
1028
1029 let params = DownloadRequest {
1030 session_id: self.id(),
1031 file_path: filename.to_string(),
1032 };
1033
1034 client
1035 .rpc("trainer.download.file".to_owned(), Some(params))
1036 .await
1037 }
1038
1039 pub async fn upload(
1040 &self,
1041 client: &client::Client,
1042 files: &[(String, PathBuf)],
1043 ) -> Result<(), Error> {
1044 let mut parts = Form::new().part(
1045 "params",
1046 Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1047 );
1048
1049 for (name, path) in files {
1050 let file_part = Part::file(path).await?.file_name(name.to_owned());
1051 parts = parts.part("file", file_part);
1052 }
1053
1054 let result = client.post_multipart("trainer.upload.files", parts).await?;
1055 trace!("TrainingSession::upload: {:?}", result);
1056 Ok(())
1057 }
1058}
1059
1060#[derive(Deserialize, Clone, Debug)]
1061pub struct ValidationSession {
1062 id: ValidationSessionID,
1063 description: String,
1064 dataset_id: DatasetID,
1065 experiment_id: ExperimentID,
1066 training_session_id: TrainingSessionID,
1067 #[serde(rename = "gt_annotation_set_id")]
1068 annotation_set_id: AnnotationSetID,
1069 #[serde(deserialize_with = "validation_session_params")]
1070 params: HashMap<String, Parameter>,
1071 #[serde(rename = "docker_task")]
1072 task: Task,
1073}
1074
1075fn validation_session_params<'de, D>(
1076 deserializer: D,
1077) -> Result<HashMap<String, Parameter>, D::Error>
1078where
1079 D: Deserializer<'de>,
1080{
1081 #[derive(Deserialize)]
1082 struct ModelParams {
1083 validation: Option<HashMap<String, Parameter>>,
1084 }
1085
1086 #[derive(Deserialize)]
1087 struct ValidateParams {
1088 model: String,
1089 }
1090
1091 #[derive(Deserialize)]
1092 struct Params {
1093 model_params: ModelParams,
1094 validate_params: ValidateParams,
1095 }
1096
1097 let params = Params::deserialize(deserializer)?;
1098 let params = match params.model_params.validation {
1099 Some(mut map) => {
1100 map.insert(
1101 "model".to_string(),
1102 Parameter::String(params.validate_params.model),
1103 );
1104 map
1105 }
1106 None => HashMap::from([(
1107 "model".to_string(),
1108 Parameter::String(params.validate_params.model),
1109 )]),
1110 };
1111
1112 Ok(params)
1113}
1114
1115impl ValidationSession {
1116 pub fn id(&self) -> ValidationSessionID {
1117 self.id
1118 }
1119
1120 pub fn name(&self) -> &str {
1121 self.task.name()
1122 }
1123
1124 pub fn description(&self) -> &str {
1125 &self.description
1126 }
1127
1128 pub fn dataset_id(&self) -> DatasetID {
1129 self.dataset_id
1130 }
1131
1132 pub fn experiment_id(&self) -> ExperimentID {
1133 self.experiment_id
1134 }
1135
1136 pub fn training_session_id(&self) -> TrainingSessionID {
1137 self.training_session_id
1138 }
1139
1140 pub fn annotation_set_id(&self) -> AnnotationSetID {
1141 self.annotation_set_id
1142 }
1143
1144 pub fn params(&self) -> &HashMap<String, Parameter> {
1145 &self.params
1146 }
1147
1148 pub fn task(&self) -> &Task {
1149 &self.task
1150 }
1151
1152 pub async fn metrics(
1153 &self,
1154 client: &client::Client,
1155 ) -> Result<HashMap<String, Parameter>, Error> {
1156 #[derive(Deserialize)]
1157 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1158 enum Response {
1159 Empty {},
1160 Map(HashMap<String, Parameter>),
1161 String(String),
1162 }
1163
1164 let params = HashMap::from([("validate_session_id", self.id().value())]);
1165 let resp: Response = client
1166 .rpc("validate.session.metrics".to_owned(), Some(params))
1167 .await?;
1168
1169 Ok(match resp {
1170 Response::String(metrics) => serde_json::from_str(&metrics)?,
1171 Response::Map(metrics) => metrics,
1172 Response::Empty {} => HashMap::new(),
1173 })
1174 }
1175
1176 pub async fn set_metrics(
1177 &self,
1178 client: &client::Client,
1179 metrics: HashMap<String, Parameter>,
1180 ) -> Result<(), Error> {
1181 let metrics = PublishMetrics {
1182 trainer_session_id: None,
1183 validate_session_id: Some(self.id()),
1184 metrics,
1185 };
1186
1187 let _: String = client
1188 .rpc("validate.session.metrics".to_owned(), Some(metrics))
1189 .await?;
1190
1191 Ok(())
1192 }
1193
1194 pub async fn upload_data(
1219 &self,
1220 client: &client::Client,
1221 files: &[(String, std::path::PathBuf)],
1222 folder: Option<&str>,
1223 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1224 ) -> Result<(), Error> {
1225 use futures::StreamExt;
1226 use std::sync::{
1227 Arc,
1228 atomic::{AtomicUsize, Ordering},
1229 };
1230 use tokio_util::io::ReaderStream;
1231
1232 let mut total: usize = 0;
1234 let mut file_meta = Vec::with_capacity(files.len());
1235 for (name, path) in files {
1236 let f = tokio::fs::File::open(path).await?;
1237 let len = f.metadata().await?.len() as usize;
1238 total += len;
1239 file_meta.push((name.clone(), f, len));
1240 }
1241
1242 let sent = Arc::new(AtomicUsize::new(0));
1244
1245 let mut form = Form::new().text("session_id", self.id().value().to_string());
1246 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
1247 form = form.text("folder", folder.to_owned());
1248 }
1249
1250 for (name, file, len) in file_meta {
1251 let reader_stream = ReaderStream::new(file);
1252 let sent_clone = sent.clone();
1253 let progress_clone = progress.clone();
1254 let progress_stream = reader_stream.inspect(move |chunk_result| {
1255 if let Ok(chunk) = chunk_result {
1256 let current =
1257 sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1258 if let Some(tx) = &progress_clone {
1263 let _ = tx.try_send(Progress {
1264 current,
1265 total,
1266 status: None,
1267 });
1268 }
1269 }
1270 });
1271 let body = reqwest::Body::wrap_stream(progress_stream);
1272 let part = Part::stream_with_length(body, len as u64).file_name(name);
1273 form = form.part("file", part);
1274 }
1275
1276 let result = match client.post_multipart("val.data.upload", form).await {
1277 Ok(_) => Ok(()),
1278 Err(Error::RpcError(code, msg)) => {
1279 Err(client::map_rpc_error("val.data.upload", code, msg, None))
1280 }
1281 Err(e) => Err(e),
1282 };
1283
1284 if result.is_ok()
1289 && let Some(tx) = progress
1290 {
1291 let _ = tx
1292 .send(Progress {
1293 current: total,
1294 total,
1295 status: None,
1296 })
1297 .await;
1298 }
1299 result
1300 }
1301
1302 pub async fn download_data(
1322 &self,
1323 client: &client::Client,
1324 filename: &str,
1325 output_path: &std::path::Path,
1326 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1327 ) -> Result<(), Error> {
1328 let req = client::ValDataDownloadRequest {
1329 session_id: self.id().value(),
1330 filename: filename.to_owned(),
1331 };
1332 match client
1333 .rpc_download("val.data.download", &req, output_path, progress)
1334 .await
1335 {
1336 Ok(()) => Ok(()),
1337 Err(Error::RpcError(code, msg)) => {
1338 Err(client::map_rpc_error("val.data.download", code, msg, None))
1339 }
1340 Err(e) => Err(e),
1341 }
1342 }
1343
1344 pub async fn data_list(&self, client: &client::Client) -> Result<Vec<String>, Error> {
1359 let req = client::ValDataListRequest {
1360 session_id: self.id().value(),
1361 };
1362 match client.rpc("val.data.list".to_owned(), Some(&req)).await {
1363 Ok(r) => Ok(r),
1364 Err(Error::RpcError(code, msg)) => {
1365 Err(client::map_rpc_error("val.data.list", code, msg, None))
1366 }
1367 Err(e) => Err(e),
1368 }
1369 }
1370}
1371
1372#[derive(Debug, Clone)]
1391pub struct StartValidationRequest {
1392 pub project_id: ProjectID,
1393 pub name: String,
1394 pub training_session_id: TrainingSessionID,
1395 pub model_file: String,
1396 pub val_type: String,
1397 pub params: HashMap<String, Parameter>,
1398 pub is_local: bool,
1399 pub is_kubernetes: bool,
1400 pub description: Option<String>,
1401 pub dataset_id: Option<DatasetID>,
1402 pub annotation_set_id: Option<AnnotationSetID>,
1403 pub snapshot_id: Option<SnapshotID>,
1404}
1405
1406#[derive(Deserialize, Debug, Clone)]
1421pub struct NewValidationSession {
1422 #[serde(rename = "id")]
1423 pub task_id: TaskID,
1424 #[serde(rename = "val_session_id", default)]
1425 pub session_id: Option<ValidationSessionID>,
1426}
1427
1428impl Display for NewValidationSession {
1429 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1430 match self.session_id {
1431 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1432 None => write!(f, "task {} (no session)", self.task_id),
1433 }
1434 }
1435}
1436
1437#[derive(Debug, Clone)]
1457pub struct StartTrainingRequest {
1458 pub project_id: ProjectID,
1460 pub name: String,
1462 pub experiment_id: ExperimentID,
1464 pub trainer_type: String,
1467 pub dataset_id: DatasetID,
1469 pub annotation_set_id: AnnotationSetID,
1471 pub tag_name: Option<String>,
1475 pub train_group: Option<String>,
1477 pub val_group: Option<String>,
1479 pub session_name: Option<String>,
1482 pub session_description: Option<String>,
1484 pub weights_session: Option<TrainingSessionID>,
1486 pub params: HashMap<String, Parameter>,
1488 pub is_local: bool,
1490 pub is_kubernetes: bool,
1492}
1493
1494#[derive(Deserialize, Debug, Clone)]
1507pub struct NewTrainingSession {
1508 #[serde(rename = "id")]
1509 pub task_id: TaskID,
1510 #[serde(rename = "train_session_id", default)]
1511 pub session_id: Option<TrainingSessionID>,
1512}
1513
1514impl Display for NewTrainingSession {
1515 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1516 match self.session_id {
1517 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1518 None => write!(f, "task {} (no session)", self.task_id),
1519 }
1520 }
1521}
1522
1523#[derive(Deserialize, Debug, Clone)]
1531pub struct Tag {
1532 pub id: u64,
1534 pub name: String,
1536 #[serde(default)]
1538 pub dataset_id: u64,
1539}
1540
1541#[derive(Deserialize, Clone, Debug)]
1542pub struct DatasetParams {
1543 dataset_id: DatasetID,
1544 annotation_set_id: AnnotationSetID,
1545 #[serde(rename = "train_group_name")]
1546 train_group: String,
1547 #[serde(rename = "val_group_name")]
1548 val_group: String,
1549}
1550
1551impl DatasetParams {
1552 pub fn dataset_id(&self) -> DatasetID {
1553 self.dataset_id
1554 }
1555
1556 pub fn annotation_set_id(&self) -> AnnotationSetID {
1557 self.annotation_set_id
1558 }
1559
1560 pub fn train_group(&self) -> &str {
1561 &self.train_group
1562 }
1563
1564 pub fn val_group(&self) -> &str {
1565 &self.val_group
1566 }
1567}
1568
1569#[derive(Serialize, Debug, Clone)]
1570pub struct TasksListParams {
1571 #[serde(skip_serializing_if = "Option::is_none")]
1572 pub continue_token: Option<String>,
1573 #[serde(skip_serializing_if = "Option::is_none")]
1574 pub types: Option<Vec<String>>,
1575 #[serde(rename = "manage_types", skip_serializing_if = "Option::is_none")]
1576 pub manager: Option<Vec<String>>,
1577 #[serde(skip_serializing_if = "Option::is_none")]
1578 pub status: Option<Vec<String>>,
1579}
1580
1581#[derive(Debug, Clone, Serialize, Deserialize)]
1587pub struct TaskDataList {
1588 pub server: String,
1589 #[serde(rename = "organization_uid")]
1590 pub organization_uid: String,
1591 #[serde(default)]
1592 pub traces: Vec<String>,
1593 #[serde(default)]
1594 pub data: std::collections::HashMap<String, Vec<String>>,
1595}
1596
1597#[derive(Debug, Clone, Serialize, Deserialize)]
1602pub struct Job {
1603 #[serde(default)]
1605 pub code: String,
1606 #[serde(default)]
1608 pub title: String,
1609 #[serde(default)]
1611 pub job_name: String,
1612 #[serde(default)]
1614 pub job_id: String,
1615 #[serde(default)]
1617 pub state: String,
1618 #[serde(default)]
1620 pub launch: Option<DateTime<Utc>>,
1621 pub task_id: i64,
1626}
1627
1628impl Job {
1629 pub fn task_id(&self) -> TaskID {
1635 TaskID::from(self.task_id.max(0) as u64)
1636 }
1637}
1638
1639#[derive(Deserialize, Debug, Clone)]
1640pub struct TasksListResult {
1641 pub tasks: Vec<Task>,
1642 pub continue_token: Option<String>,
1643}
1644
1645#[derive(Deserialize, Debug, Clone)]
1646pub struct Task {
1647 id: TaskID,
1648 name: String,
1649 #[serde(rename = "type")]
1650 workflow: String,
1651 status: String,
1652 #[serde(rename = "manage_type")]
1653 manager: Option<String>,
1654 #[serde(rename = "instance_type")]
1655 instance: String,
1656 #[serde(rename = "date")]
1657 created: DateTime<Utc>,
1658}
1659
1660impl Task {
1661 pub fn id(&self) -> TaskID {
1662 self.id
1663 }
1664
1665 pub fn name(&self) -> &str {
1666 &self.name
1667 }
1668
1669 pub fn workflow(&self) -> &str {
1670 &self.workflow
1671 }
1672
1673 pub fn status(&self) -> &str {
1674 &self.status
1675 }
1676
1677 pub fn manager(&self) -> Option<&str> {
1678 self.manager.as_deref()
1679 }
1680
1681 pub fn instance(&self) -> &str {
1682 &self.instance
1683 }
1684
1685 pub fn created(&self) -> &DateTime<Utc> {
1686 &self.created
1687 }
1688}
1689
1690impl Display for Task {
1691 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1692 write!(
1693 f,
1694 "{} [{:?} {}] {}",
1695 self.id,
1696 self.manager(),
1697 self.workflow(),
1698 self.name()
1699 )
1700 }
1701}
1702
1703#[derive(Deserialize, Debug, Clone)]
1704pub struct TaskInfo {
1705 id: TaskID,
1706 project_id: Option<ProjectID>,
1707 #[serde(rename = "task_description", alias = "description", default)]
1708 description: String,
1709 #[serde(rename = "type")]
1710 workflow: String,
1711 status: Option<String>,
1712 #[serde(default)]
1713 progress: TaskProgress,
1714 #[serde(
1715 rename = "created_date",
1716 alias = "created",
1717 default = "default_datetime_utc"
1718 )]
1719 created: DateTime<Utc>,
1720 #[serde(
1721 rename = "end_date",
1722 alias = "completed",
1723 default = "default_datetime_utc"
1724 )]
1725 completed: DateTime<Utc>,
1726}
1727
1728fn default_datetime_utc() -> DateTime<Utc> {
1729 DateTime::UNIX_EPOCH
1730}
1731
1732impl Display for TaskInfo {
1733 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1734 write!(f, "{} {}: {}", self.id, self.workflow(), self.description())
1735 }
1736}
1737
1738impl TaskInfo {
1739 pub fn id(&self) -> TaskID {
1740 self.id
1741 }
1742
1743 pub fn project_id(&self) -> Option<ProjectID> {
1744 self.project_id
1745 }
1746
1747 pub fn description(&self) -> &str {
1748 &self.description
1749 }
1750
1751 pub fn workflow(&self) -> &str {
1752 &self.workflow
1753 }
1754
1755 pub fn status(&self) -> &Option<String> {
1756 &self.status
1757 }
1758
1759 pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1760 let t = client.task_status(self.id(), status).await?;
1761 self.status = Some(t.status);
1762 Ok(())
1763 }
1764
1765 pub fn stages(&self) -> HashMap<String, Stage> {
1766 match &self.progress.stages {
1767 Some(stages) => stages.clone(),
1768 None => HashMap::new(),
1769 }
1770 }
1771
1772 pub async fn update_stage(
1773 &mut self,
1774 client: &Client,
1775 stage: &str,
1776 status: &str,
1777 message: &str,
1778 percentage: u8,
1779 ) -> Result<(), Error> {
1780 client
1781 .update_stage(self.id(), stage, status, message, percentage)
1782 .await?;
1783 let t = client.task_info(self.id()).await?;
1784 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1785 Ok(())
1786 }
1787
1788 pub async fn set_stages(
1789 &mut self,
1790 client: &Client,
1791 stages: &[(&str, &str)],
1792 ) -> Result<(), Error> {
1793 client.set_stages(self.id(), stages).await?;
1794 let t = client.task_info(self.id()).await?;
1795 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1796 Ok(())
1797 }
1798
1799 pub async fn data_list(&self, client: &client::Client) -> Result<TaskDataList, Error> {
1815 let req = client::TaskDataListRequest {
1816 task_id: self.id().value(),
1817 };
1818 match client.rpc("task.data.list".to_owned(), Some(&req)).await {
1819 Ok(r) => Ok(r),
1820 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1821 "task.data.list",
1822 code,
1823 msg,
1824 Some(self.id()),
1825 )),
1826 Err(e) => Err(e),
1827 }
1828 }
1829
1830 pub async fn upload_data(
1851 &self,
1852 client: &client::Client,
1853 path: &std::path::Path,
1854 folder: Option<&str>,
1855 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1856 ) -> Result<(), Error> {
1857 use futures::StreamExt;
1858 use std::sync::{
1859 Arc,
1860 atomic::{AtomicUsize, Ordering},
1861 };
1862 use tokio_util::io::ReaderStream;
1863
1864 let file_name = path
1865 .file_name()
1866 .and_then(|s| s.to_str())
1867 .ok_or_else(|| Error::InvalidParameters("path must have a UTF-8 filename".into()))?
1868 .to_owned();
1869
1870 let file = tokio::fs::File::open(path).await?;
1871 let total = file.metadata().await?.len() as usize;
1872 let sent = Arc::new(AtomicUsize::new(0));
1873
1874 let reader_stream = ReaderStream::new(file);
1875 let sent_clone = sent.clone();
1876 let progress_clone = progress.clone();
1877 let progress_stream = reader_stream.inspect(move |chunk_result| {
1878 if let Ok(chunk) = chunk_result {
1879 let current = sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1880 if let Some(tx) = &progress_clone {
1886 let _ = tx.try_send(Progress {
1887 current,
1888 total,
1889 status: None,
1890 });
1891 }
1892 }
1893 });
1894
1895 let body = reqwest::Body::wrap_stream(progress_stream);
1896 let file_part = Part::stream_with_length(body, total as u64).file_name(file_name);
1897
1898 let mut form = Form::new().text("task_id", self.id().value().to_string());
1899 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
1900 form = form.text("folder", folder.to_owned());
1901 }
1902 form = form.part("file", file_part);
1903
1904 let result = match client.post_multipart("task.data.upload", form).await {
1905 Ok(_) => Ok(()),
1906 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1907 "task.data.upload",
1908 code,
1909 msg,
1910 Some(self.id()),
1911 )),
1912 Err(e) => Err(e),
1913 };
1914
1915 if result.is_ok()
1919 && let Some(tx) = progress
1920 {
1921 let _ = tx
1922 .send(Progress {
1923 current: total,
1924 total,
1925 status: None,
1926 })
1927 .await;
1928 }
1929 result
1930 }
1931
1932 pub async fn download_data(
1961 &self,
1962 client: &client::Client,
1963 file: &str,
1964 folder: Option<&str>,
1965 output_path: &std::path::Path,
1966 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1967 ) -> Result<(), Error> {
1968 let folder = folder.unwrap_or("").to_owned();
1969 let req = client::TaskDataDownloadRequest {
1970 task_id: self.id().value(),
1971 folder,
1972 file: file.to_owned(),
1973 };
1974 match client
1975 .rpc_download("task.data.download", &req, output_path, progress)
1976 .await
1977 {
1978 Ok(()) => Ok(()),
1979 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1980 "task.data.download",
1981 code,
1982 msg,
1983 Some(self.id()),
1984 )),
1985 Err(e) => Err(e),
1986 }
1987 }
1988
1989 pub async fn add_chart(
2017 &self,
2018 client: &client::Client,
2019 group: &str,
2020 name: &str,
2021 data: Parameter,
2022 params: Option<Parameter>,
2023 ) -> Result<(), Error> {
2024 client::validate_chart_args(group, name)?;
2025 let req = client::TaskChartAddRequest {
2026 task_id: self.id().value(),
2027 group_name: group.to_owned(),
2028 chart_name: name.to_owned(),
2029 params,
2030 data,
2031 };
2032 let _resp: serde_json::Value =
2033 match client.rpc("task.chart.add".to_owned(), Some(&req)).await {
2034 Ok(r) => r,
2035 Err(Error::RpcError(code, msg)) => {
2036 return Err(client::map_rpc_error(
2037 "task.chart.add",
2038 code,
2039 msg,
2040 Some(self.id()),
2041 ));
2042 }
2043 Err(e) => return Err(e),
2044 };
2045 Ok(())
2046 }
2047
2048 pub async fn list_charts(
2065 &self,
2066 client: &client::Client,
2067 group: Option<&str>,
2068 ) -> Result<TaskDataList, Error> {
2069 let req = client::TaskChartListRequest {
2070 task_id: self.id().value(),
2071 group_name: group.unwrap_or("").to_owned(),
2072 };
2073 match client.rpc("task.chart.list".to_owned(), Some(&req)).await {
2074 Ok(r) => Ok(r),
2075 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2076 "task.chart.list",
2077 code,
2078 msg,
2079 Some(self.id()),
2080 )),
2081 Err(e) => Err(e),
2082 }
2083 }
2084
2085 pub async fn get_chart(
2104 &self,
2105 client: &client::Client,
2106 group: &str,
2107 name: &str,
2108 ) -> Result<Parameter, Error> {
2109 client::validate_chart_args(group, name)?;
2110 let req = client::TaskChartGetRequest {
2111 task_id: self.id().value(),
2112 group_name: group.to_owned(),
2113 chart_name: name.to_owned(),
2114 };
2115 match client.rpc("task.chart.get".to_owned(), Some(&req)).await {
2116 Ok(r) => Ok(r),
2117 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2118 "task.chart.get",
2119 code,
2120 msg,
2121 Some(self.id()),
2122 )),
2123 Err(e) => Err(e),
2124 }
2125 }
2126
2127 pub fn created(&self) -> &DateTime<Utc> {
2128 &self.created
2129 }
2130
2131 pub fn completed(&self) -> &DateTime<Utc> {
2132 &self.completed
2133 }
2134}
2135
2136#[derive(Deserialize, Debug, Default, Clone)]
2137pub struct TaskProgress {
2138 stages: Option<HashMap<String, Stage>>,
2139}
2140
2141#[derive(Serialize, Debug, Clone)]
2142pub struct TaskStatus {
2143 #[serde(rename = "docker_task_id")]
2144 pub task_id: TaskID,
2145 pub status: String,
2146}
2147
2148#[derive(Serialize, Deserialize, Debug, Clone)]
2149pub struct Stage {
2150 #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
2151 task_id: Option<TaskID>,
2152 stage: String,
2153 #[serde(skip_serializing_if = "Option::is_none")]
2154 status: Option<String>,
2155 #[serde(skip_serializing_if = "Option::is_none")]
2156 description: Option<String>,
2157 #[serde(skip_serializing_if = "Option::is_none")]
2158 message: Option<String>,
2159 percentage: u8,
2160}
2161
2162impl Stage {
2163 pub fn new(
2164 task_id: Option<TaskID>,
2165 stage: String,
2166 status: Option<String>,
2167 message: Option<String>,
2168 percentage: u8,
2169 ) -> Self {
2170 Stage {
2171 task_id,
2172 stage,
2173 status,
2174 description: None,
2175 message,
2176 percentage,
2177 }
2178 }
2179
2180 pub fn task_id(&self) -> &Option<TaskID> {
2181 &self.task_id
2182 }
2183
2184 pub fn stage(&self) -> &str {
2185 &self.stage
2186 }
2187
2188 pub fn status(&self) -> &Option<String> {
2189 &self.status
2190 }
2191
2192 pub fn description(&self) -> &Option<String> {
2193 &self.description
2194 }
2195
2196 pub fn message(&self) -> &Option<String> {
2197 &self.message
2198 }
2199
2200 pub fn percentage(&self) -> u8 {
2201 self.percentage
2202 }
2203}
2204
2205#[derive(Serialize, Debug)]
2206pub struct TaskStages {
2207 #[serde(rename = "docker_task_id")]
2208 pub task_id: TaskID,
2209 #[serde(skip_serializing_if = "Vec::is_empty")]
2210 pub stages: Vec<HashMap<String, String>>,
2211}
2212
2213#[derive(Deserialize, Debug)]
2214pub struct Artifact {
2215 name: String,
2216 #[serde(rename = "modelType")]
2217 model_type: String,
2218}
2219
2220impl Artifact {
2221 pub fn name(&self) -> &str {
2222 &self.name
2223 }
2224
2225 pub fn model_type(&self) -> &str {
2226 &self.model_type
2227 }
2228}
2229
2230#[derive(Deserialize, Serialize, Clone, Debug)]
2238pub struct VersionTag {
2239 id: u64,
2240 dataset_id: DatasetID,
2241 name: String,
2242 serial: u64,
2243 #[serde(default)]
2244 description: String,
2245 created_by: String,
2246 created_at: DateTime<Utc>,
2247 #[serde(default)]
2248 image_count: u64,
2249 #[serde(default)]
2250 annotation_counts: HashMap<String, u64>,
2251 #[serde(default)]
2252 sensor_counts: HashMap<String, u64>,
2253 #[serde(default)]
2254 label_count: u64,
2255 #[serde(default)]
2256 annotation_set_count: u64,
2257 #[serde(default)]
2258 snapshot_id: Option<u64>,
2259 #[serde(default)]
2260 is_current: bool,
2261}
2262
2263impl VersionTag {
2264 pub fn id(&self) -> u64 {
2266 self.id
2267 }
2268
2269 pub fn dataset_id(&self) -> DatasetID {
2271 self.dataset_id
2272 }
2273
2274 pub fn name(&self) -> &str {
2276 &self.name
2277 }
2278
2279 pub fn serial(&self) -> u64 {
2281 self.serial
2282 }
2283
2284 pub fn description(&self) -> &str {
2286 &self.description
2287 }
2288
2289 pub fn created_by(&self) -> &str {
2291 &self.created_by
2292 }
2293
2294 pub fn created_at(&self) -> DateTime<Utc> {
2296 self.created_at
2297 }
2298
2299 pub fn image_count(&self) -> u64 {
2301 self.image_count
2302 }
2303
2304 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2306 &self.annotation_counts
2307 }
2308
2309 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2311 &self.sensor_counts
2312 }
2313
2314 pub fn label_count(&self) -> u64 {
2316 self.label_count
2317 }
2318
2319 pub fn annotation_set_count(&self) -> u64 {
2321 self.annotation_set_count
2322 }
2323
2324 pub fn snapshot_id(&self) -> Option<u64> {
2326 self.snapshot_id
2327 }
2328
2329 pub fn is_current(&self) -> bool {
2332 self.is_current
2333 }
2334}
2335
2336impl Display for VersionTag {
2337 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2338 write!(f, "{} (serial {})", self.name, self.serial)
2339 }
2340}
2341
2342#[derive(Deserialize, Serialize, Clone, Debug)]
2344pub struct ChangelogEntry {
2345 id: u64,
2346 dataset_id: DatasetID,
2347 serial: u64,
2348 entity_type: String,
2349 operation: String,
2350 #[serde(default)]
2351 entity_id: Option<u64>,
2352 #[serde(default)]
2353 change_data: serde_json::Value,
2354 username: String,
2355 organization_id: u64,
2356 created_at: DateTime<Utc>,
2357 #[serde(default)]
2358 message: String,
2359 #[serde(default, deserialize_with = "deserialize_null_as_default")]
2360 s3_version_ids: Vec<serde_json::Value>,
2361}
2362
2363impl ChangelogEntry {
2364 pub fn id(&self) -> u64 {
2365 self.id
2366 }
2367
2368 pub fn dataset_id(&self) -> DatasetID {
2369 self.dataset_id
2370 }
2371
2372 pub fn serial(&self) -> u64 {
2374 self.serial
2375 }
2376
2377 pub fn entity_type(&self) -> &str {
2379 &self.entity_type
2380 }
2381
2382 pub fn operation(&self) -> &str {
2384 &self.operation
2385 }
2386
2387 pub fn entity_id(&self) -> Option<u64> {
2388 self.entity_id
2389 }
2390
2391 pub fn change_data(&self) -> &serde_json::Value {
2393 &self.change_data
2394 }
2395
2396 pub fn username(&self) -> &str {
2397 &self.username
2398 }
2399
2400 pub fn organization_id(&self) -> u64 {
2401 self.organization_id
2402 }
2403
2404 pub fn created_at(&self) -> DateTime<Utc> {
2405 self.created_at
2406 }
2407
2408 pub fn message(&self) -> &str {
2409 &self.message
2410 }
2411
2412 pub fn s3_version_ids(&self) -> &[serde_json::Value] {
2413 &self.s3_version_ids
2414 }
2415}
2416
2417#[derive(Deserialize, Debug, Clone)]
2419pub struct ChangelogResponse {
2420 pub entries: Vec<ChangelogEntry>,
2421 pub count: u64,
2422 #[serde(default)]
2423 pub continue_token: String,
2424 #[serde(default)]
2425 pub from_serial: Option<u64>,
2426 #[serde(default)]
2427 pub to_serial: Option<u64>,
2428}
2429
2430#[derive(Deserialize, Serialize, Clone, Debug)]
2432pub struct DatasetSummary {
2433 dataset_id: DatasetID,
2434 current_serial: u64,
2435 #[serde(default)]
2436 image_count: u64,
2437 #[serde(default)]
2438 annotation_counts: HashMap<String, u64>,
2439 #[serde(default)]
2440 sensor_counts: HashMap<String, u64>,
2441 #[serde(default)]
2442 label_count: u64,
2443 #[serde(default)]
2444 annotation_set_count: u64,
2445 last_updated: DateTime<Utc>,
2446}
2447
2448impl DatasetSummary {
2449 pub fn dataset_id(&self) -> DatasetID {
2450 self.dataset_id
2451 }
2452
2453 pub fn current_serial(&self) -> u64 {
2454 self.current_serial
2455 }
2456
2457 pub fn image_count(&self) -> u64 {
2458 self.image_count
2459 }
2460
2461 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2462 &self.annotation_counts
2463 }
2464
2465 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2466 &self.sensor_counts
2467 }
2468
2469 pub fn label_count(&self) -> u64 {
2470 self.label_count
2471 }
2472
2473 pub fn annotation_set_count(&self) -> u64 {
2474 self.annotation_set_count
2475 }
2476
2477 pub fn last_updated(&self) -> DateTime<Utc> {
2478 self.last_updated
2479 }
2480}
2481
2482#[derive(Deserialize, Debug, Clone)]
2484pub struct VersionCurrentResponse {
2485 pub dataset_id: DatasetID,
2486 pub current_serial: u64,
2487 #[serde(default)]
2488 pub latest_tag: Option<VersionTag>,
2489 #[serde(default)]
2490 pub tags: Vec<VersionTag>,
2491 #[serde(default)]
2492 pub summary: Option<DatasetSummary>,
2493}
2494
2495#[derive(Deserialize, Debug, Clone)]
2497pub struct RestoredFrom {
2498 pub tag: String,
2499 pub serial: u64,
2500}
2501
2502#[derive(Deserialize, Debug, Clone)]
2504pub struct RestoredCounts {
2505 pub images: u64,
2506 pub labels: u64,
2507 pub annotation_sets: u64,
2508}
2509
2510#[derive(Deserialize, Debug, Clone)]
2512pub struct RestoreResult {
2513 pub success: bool,
2514 pub new_serial: u64,
2515 pub restored_from: RestoredFrom,
2516 pub restored_counts: RestoredCounts,
2517 pub message: String,
2518}
2519
2520#[derive(Serialize)]
2523pub(crate) struct VersionTagCreateParams {
2524 pub dataset_id: DatasetID,
2525 pub name: String,
2526 #[serde(skip_serializing_if = "Option::is_none")]
2527 pub description: Option<String>,
2528}
2529
2530#[derive(Serialize)]
2531pub(crate) struct VersionTagNameParams {
2532 pub dataset_id: DatasetID,
2533 pub name: String,
2534}
2535
2536#[derive(Serialize)]
2537pub(crate) struct VersionChangelogParams {
2538 pub dataset_id: DatasetID,
2539 #[serde(skip_serializing_if = "Option::is_none")]
2540 pub from_version: Option<String>,
2541 #[serde(skip_serializing_if = "Option::is_none")]
2542 pub to_version: Option<String>,
2543 #[serde(skip_serializing_if = "Option::is_none")]
2544 pub entity_types: Option<Vec<String>>,
2545 #[serde(skip_serializing_if = "Option::is_none")]
2546 pub limit: Option<u64>,
2547 #[serde(skip_serializing_if = "Option::is_none")]
2548 pub continue_token: Option<String>,
2549}
2550
2551#[derive(Deserialize, Debug)]
2553pub(crate) struct ChangelogCountResult {
2554 pub count: u64,
2555}
2556
2557#[derive(Serialize, Deserialize, Debug, Clone)]
2564pub struct TrainerSchemaInfo {
2565 pub name: String,
2567 #[serde(default)]
2569 pub label: String,
2570 #[serde(default)]
2572 pub schema_type: String,
2573}
2574
2575#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
2581#[serde(rename_all = "lowercase")]
2582pub enum SchemaFieldType {
2583 Group,
2585 Slider,
2587 Select,
2589 Bool,
2591 Int,
2593 Float,
2595 Text,
2597 Date,
2599 Project,
2601 Dataset,
2603 Trainer,
2605 Upload,
2607 Info,
2610 #[serde(other)]
2612 Unknown,
2613}
2614
2615fn lenient_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
2619where
2620 D: Deserializer<'de>,
2621{
2622 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
2623 Ok(value.map(|v| match v {
2624 serde_json::Value::String(s) => s,
2625 other => other.to_string(),
2626 }))
2627}
2628
2629#[derive(Serialize, Deserialize, Debug, Clone)]
2631pub struct SchemaOption {
2632 #[serde(default)]
2634 pub name: Option<Parameter>,
2635 #[serde(default, deserialize_with = "lenient_string")]
2638 pub label: Option<String>,
2639 #[serde(default)]
2641 pub children: Vec<SchemaField>,
2642}
2643
2644#[derive(Serialize, Deserialize, Debug, Clone)]
2656pub struct SchemaField {
2657 #[serde(default, deserialize_with = "lenient_string")]
2659 pub name: Option<String>,
2660 #[serde(default, deserialize_with = "lenient_string")]
2662 pub label: Option<String>,
2663 #[serde(default, deserialize_with = "lenient_string")]
2665 pub description: Option<String>,
2666 #[serde(default)]
2668 pub required: bool,
2669 #[serde(default)]
2671 pub default: Option<Parameter>,
2672 #[serde(rename = "type", default)]
2674 pub field_type: Option<SchemaFieldType>,
2675 #[serde(default)]
2677 pub min: Option<f64>,
2678 #[serde(default)]
2680 pub max: Option<f64>,
2681 #[serde(default)]
2683 pub step: Option<f64>,
2684 #[serde(default)]
2686 pub options: Vec<SchemaOption>,
2687 #[serde(default)]
2690 pub children: Vec<SchemaField>,
2691 #[serde(default)]
2693 pub is_dropdown: bool,
2694 #[serde(default)]
2696 pub multi_select: bool,
2697 #[serde(default)]
2699 pub is_multi_line: bool,
2700 #[serde(default)]
2702 pub hidden: bool,
2703 #[serde(default)]
2705 pub numeric_only: bool,
2706 #[serde(default)]
2708 pub enable_tags_selection: bool,
2709 #[serde(default)]
2711 pub enable_annotation_set_selection: bool,
2712 #[serde(default)]
2714 pub values: Option<Vec<Parameter>>,
2715}
2716
2717#[derive(Serialize, Deserialize, Debug, Clone)]
2720pub struct ValidatorSchema {
2721 #[serde(rename = "type", default)]
2723 pub schema_type: String,
2724 #[serde(default)]
2726 pub name: String,
2727 #[serde(default)]
2729 pub schema: Vec<SchemaField>,
2730}
2731
2732#[cfg(test)]
2733mod tests {
2734 use super::*;
2735
2736 #[test]
2738 fn test_organization_id_from_u64() {
2739 let id = OrganizationID::from(12345);
2740 assert_eq!(id.value(), 12345);
2741 }
2742
2743 #[test]
2744 fn test_organization_id_display() {
2745 let id = OrganizationID::from(0xabc123);
2746 assert_eq!(format!("{}", id), "org-abc123");
2747 }
2748
2749 #[test]
2750 fn test_organization_id_try_from_str_valid() {
2751 let id = OrganizationID::try_from("org-abc123").unwrap();
2752 assert_eq!(id.value(), 0xabc123);
2753 }
2754
2755 #[test]
2756 fn test_organization_id_try_from_str_invalid_prefix() {
2757 let result = OrganizationID::try_from("invalid-abc123");
2758 assert!(result.is_err());
2759 match result {
2760 Err(Error::InvalidParameters(msg)) => {
2761 assert!(msg.contains("must start with 'org-'"));
2762 }
2763 _ => panic!("Expected InvalidParameters error"),
2764 }
2765 }
2766
2767 #[test]
2768 fn test_organization_id_try_from_str_invalid_hex() {
2769 let result = OrganizationID::try_from("org-xyz");
2770 assert!(result.is_err());
2771 }
2772
2773 #[test]
2774 fn test_organization_id_try_from_str_empty() {
2775 let result = OrganizationID::try_from("org-");
2776 assert!(result.is_err());
2777 }
2778
2779 #[test]
2780 fn test_organization_id_into_u64() {
2781 let id = OrganizationID::from(54321);
2782 let value: u64 = id.into();
2783 assert_eq!(value, 54321);
2784 }
2785
2786 #[test]
2788 fn test_usage_summary_deserialize_and_accessors() {
2789 let usage: UsageSummary = serde_json::from_str(
2790 r#"{"credits": 12.5, "funds": 49092.92, "total_funds_and_credits": 49105.42}"#,
2791 )
2792 .unwrap();
2793 assert_eq!(usage.credits(), 12.5);
2794 assert_eq!(usage.funds(), 49092.92);
2795 assert_eq!(usage.total(), 49105.42);
2796 }
2797
2798 #[test]
2799 fn test_usage_summary_defaults_for_missing_fields() {
2800 let usage: UsageSummary = serde_json::from_str("{}").unwrap();
2804 assert_eq!(usage.credits(), 0.0);
2805 assert_eq!(usage.funds(), 0.0);
2806 assert_eq!(usage.total(), 0.0);
2807 }
2808
2809 #[test]
2811 fn test_project_id_from_u64() {
2812 let id = ProjectID::from(78910);
2813 assert_eq!(id.value(), 78910);
2814 }
2815
2816 #[test]
2817 fn test_project_id_display() {
2818 let id = ProjectID::from(0xdef456);
2819 assert_eq!(format!("{}", id), "p-def456");
2820 }
2821
2822 #[test]
2823 fn test_project_id_from_str_valid() {
2824 let id = ProjectID::from_str("p-def456").unwrap();
2825 assert_eq!(id.value(), 0xdef456);
2826 }
2827
2828 #[test]
2829 fn test_project_id_try_from_str_valid() {
2830 let id = ProjectID::try_from("p-123abc").unwrap();
2831 assert_eq!(id.value(), 0x123abc);
2832 }
2833
2834 #[test]
2835 fn test_project_id_try_from_string_valid() {
2836 let id = ProjectID::try_from("p-456def".to_string()).unwrap();
2837 assert_eq!(id.value(), 0x456def);
2838 }
2839
2840 #[test]
2841 fn test_project_id_from_str_invalid_prefix() {
2842 let result = ProjectID::from_str("proj-123");
2843 assert!(result.is_err());
2844 match result {
2845 Err(Error::InvalidParameters(msg)) => {
2846 assert!(msg.contains("must start with 'p-'"));
2847 }
2848 _ => panic!("Expected InvalidParameters error"),
2849 }
2850 }
2851
2852 #[test]
2853 fn test_project_id_from_str_invalid_hex() {
2854 let result = ProjectID::from_str("p-notahex");
2855 assert!(result.is_err());
2856 }
2857
2858 #[test]
2859 fn test_project_id_into_u64() {
2860 let id = ProjectID::from(99999);
2861 let value: u64 = id.into();
2862 assert_eq!(value, 99999);
2863 }
2864
2865 #[test]
2867 fn test_experiment_id_from_u64() {
2868 let id = ExperimentID::from(1193046);
2869 assert_eq!(id.value(), 1193046);
2870 }
2871
2872 #[test]
2873 fn test_experiment_id_display() {
2874 let id = ExperimentID::from(0x123abc);
2875 assert_eq!(format!("{}", id), "exp-123abc");
2876 }
2877
2878 #[test]
2879 fn test_experiment_id_from_str_valid() {
2880 let id = ExperimentID::from_str("exp-456def").unwrap();
2881 assert_eq!(id.value(), 0x456def);
2882 }
2883
2884 #[test]
2885 fn test_experiment_id_try_from_str_valid() {
2886 let id = ExperimentID::try_from("exp-789abc").unwrap();
2887 assert_eq!(id.value(), 0x789abc);
2888 }
2889
2890 #[test]
2891 fn test_experiment_id_try_from_string_valid() {
2892 let id = ExperimentID::try_from("exp-fedcba".to_string()).unwrap();
2893 assert_eq!(id.value(), 0xfedcba);
2894 }
2895
2896 #[test]
2897 fn test_experiment_id_from_str_invalid_prefix() {
2898 let result = ExperimentID::from_str("experiment-123");
2899 assert!(result.is_err());
2900 match result {
2901 Err(Error::InvalidParameters(msg)) => {
2902 assert!(msg.contains("must start with 'exp-'"));
2903 }
2904 _ => panic!("Expected InvalidParameters error"),
2905 }
2906 }
2907
2908 #[test]
2909 fn test_experiment_id_from_str_invalid_hex() {
2910 let result = ExperimentID::from_str("exp-zzz");
2911 assert!(result.is_err());
2912 }
2913
2914 #[test]
2915 fn test_experiment_id_into_u64() {
2916 let id = ExperimentID::from(777777);
2917 let value: u64 = id.into();
2918 assert_eq!(value, 777777);
2919 }
2920
2921 #[test]
2923 fn test_training_session_id_from_u64() {
2924 let id = TrainingSessionID::from(7901234);
2925 assert_eq!(id.value(), 7901234);
2926 }
2927
2928 #[test]
2929 fn test_training_session_id_display() {
2930 let id = TrainingSessionID::from(0xabc123);
2931 assert_eq!(format!("{}", id), "t-abc123");
2932 }
2933
2934 #[test]
2935 fn test_training_session_id_from_str_valid() {
2936 let id = TrainingSessionID::from_str("t-abc123").unwrap();
2937 assert_eq!(id.value(), 0xabc123);
2938 }
2939
2940 #[test]
2941 fn test_training_session_id_try_from_str_valid() {
2942 let id = TrainingSessionID::try_from("t-deadbeef").unwrap();
2943 assert_eq!(id.value(), 0xdeadbeef);
2944 }
2945
2946 #[test]
2947 fn test_training_session_id_try_from_string_valid() {
2948 let id = TrainingSessionID::try_from("t-cafebabe".to_string()).unwrap();
2949 assert_eq!(id.value(), 0xcafebabe);
2950 }
2951
2952 #[test]
2953 fn test_training_session_id_from_str_invalid_prefix() {
2954 let result = TrainingSessionID::from_str("training-123");
2955 assert!(result.is_err());
2956 match result {
2957 Err(Error::InvalidParameters(msg)) => {
2958 assert!(msg.contains("must start with 't-'"));
2959 }
2960 _ => panic!("Expected InvalidParameters error"),
2961 }
2962 }
2963
2964 #[test]
2965 fn test_training_session_id_from_str_invalid_hex() {
2966 let result = TrainingSessionID::from_str("t-qqq");
2967 assert!(result.is_err());
2968 }
2969
2970 #[test]
2971 fn test_training_session_id_into_u64() {
2972 let id = TrainingSessionID::from(123456);
2973 let value: u64 = id.into();
2974 assert_eq!(value, 123456);
2975 }
2976
2977 #[test]
2979 fn test_validation_session_id_from_u64() {
2980 let id = ValidationSessionID::from(3456789);
2981 assert_eq!(id.value(), 3456789);
2982 }
2983
2984 #[test]
2985 fn test_validation_session_id_display() {
2986 let id = ValidationSessionID::from(0x34c985);
2987 assert_eq!(format!("{}", id), "v-34c985");
2988 }
2989
2990 #[test]
2991 fn test_validation_session_id_try_from_str_valid() {
2992 let id = ValidationSessionID::try_from("v-deadbeef").unwrap();
2993 assert_eq!(id.value(), 0xdeadbeef);
2994 }
2995
2996 #[test]
2997 fn test_validation_session_id_try_from_string_valid() {
2998 let id = ValidationSessionID::try_from("v-12345678".to_string()).unwrap();
2999 assert_eq!(id.value(), 0x12345678);
3000 }
3001
3002 #[test]
3003 fn test_validation_session_id_try_from_str_invalid_prefix() {
3004 let result = ValidationSessionID::try_from("validation-123");
3005 assert!(result.is_err());
3006 match result {
3007 Err(Error::InvalidParameters(msg)) => {
3008 assert!(msg.contains("must start with 'v-'"));
3009 }
3010 _ => panic!("Expected InvalidParameters error"),
3011 }
3012 }
3013
3014 #[test]
3015 fn test_validation_session_id_try_from_str_invalid_hex() {
3016 let result = ValidationSessionID::try_from("v-xyz");
3017 assert!(result.is_err());
3018 }
3019
3020 #[test]
3021 fn test_validation_session_id_into_u64() {
3022 let id = ValidationSessionID::from(987654);
3023 let value: u64 = id.into();
3024 assert_eq!(value, 987654);
3025 }
3026
3027 #[test]
3029 fn test_snapshot_id_from_u64() {
3030 let id = SnapshotID::from(111222);
3031 assert_eq!(id.value(), 111222);
3032 }
3033
3034 #[test]
3035 fn test_snapshot_id_display() {
3036 let id = SnapshotID::from(0xaabbcc);
3037 assert_eq!(format!("{}", id), "ss-aabbcc");
3038 }
3039
3040 #[test]
3041 fn test_snapshot_id_try_from_str_valid() {
3042 let id = SnapshotID::try_from("ss-aabbcc").unwrap();
3043 assert_eq!(id.value(), 0xaabbcc);
3044 }
3045
3046 #[test]
3047 fn test_snapshot_id_try_from_str_invalid_prefix() {
3048 let result = SnapshotID::try_from("snapshot-123");
3049 assert!(result.is_err());
3050 match result {
3051 Err(Error::InvalidParameters(msg)) => {
3052 assert!(msg.contains("must start with 'ss-'"));
3053 }
3054 _ => panic!("Expected InvalidParameters error"),
3055 }
3056 }
3057
3058 #[test]
3059 fn test_snapshot_id_try_from_str_invalid_hex() {
3060 let result = SnapshotID::try_from("ss-ggg");
3061 assert!(result.is_err());
3062 }
3063
3064 #[test]
3065 fn test_snapshot_id_into_u64() {
3066 let id = SnapshotID::from(333444);
3067 let value: u64 = id.into();
3068 assert_eq!(value, 333444);
3069 }
3070
3071 #[test]
3073 fn test_task_id_from_u64() {
3074 let id = TaskID::from(555666);
3075 assert_eq!(id.value(), 555666);
3076 }
3077
3078 #[test]
3079 fn test_task_id_display() {
3080 let id = TaskID::from(0x123456);
3081 assert_eq!(format!("{}", id), "task-123456");
3082 }
3083
3084 #[test]
3085 fn test_task_id_from_str_valid() {
3086 let id = TaskID::from_str("task-123456").unwrap();
3087 assert_eq!(id.value(), 0x123456);
3088 }
3089
3090 #[test]
3091 fn test_task_id_try_from_str_valid() {
3092 let id = TaskID::try_from("task-abcdef").unwrap();
3093 assert_eq!(id.value(), 0xabcdef);
3094 }
3095
3096 #[test]
3097 fn test_task_id_try_from_string_valid() {
3098 let id = TaskID::try_from("task-fedcba".to_string()).unwrap();
3099 assert_eq!(id.value(), 0xfedcba);
3100 }
3101
3102 #[test]
3103 fn test_task_id_from_str_invalid_prefix() {
3104 let result = TaskID::from_str("t-123");
3105 assert!(result.is_err());
3106 match result {
3107 Err(Error::InvalidParameters(msg)) => {
3108 assert!(msg.contains("must start with 'task-'"));
3109 }
3110 _ => panic!("Expected InvalidParameters error"),
3111 }
3112 }
3113
3114 #[test]
3115 fn test_task_id_from_str_invalid_hex() {
3116 let result = TaskID::from_str("task-zzz");
3117 assert!(result.is_err());
3118 }
3119
3120 #[test]
3121 fn test_task_id_into_u64() {
3122 let id = TaskID::from(777888);
3123 let value: u64 = id.into();
3124 assert_eq!(value, 777888);
3125 }
3126
3127 #[test]
3129 fn test_dataset_id_from_u64() {
3130 let id = DatasetID::from(1193046);
3131 assert_eq!(id.value(), 1193046);
3132 }
3133
3134 #[test]
3135 fn test_dataset_id_display() {
3136 let id = DatasetID::from(0x123abc);
3137 assert_eq!(format!("{}", id), "ds-123abc");
3138 }
3139
3140 #[test]
3141 fn test_dataset_id_from_str_valid() {
3142 let id = DatasetID::from_str("ds-456def").unwrap();
3143 assert_eq!(id.value(), 0x456def);
3144 }
3145
3146 #[test]
3147 fn test_dataset_id_try_from_str_valid() {
3148 let id = DatasetID::try_from("ds-789abc").unwrap();
3149 assert_eq!(id.value(), 0x789abc);
3150 }
3151
3152 #[test]
3153 fn test_dataset_id_try_from_string_valid() {
3154 let id = DatasetID::try_from("ds-fedcba".to_string()).unwrap();
3155 assert_eq!(id.value(), 0xfedcba);
3156 }
3157
3158 #[test]
3159 fn test_dataset_id_from_str_invalid_prefix() {
3160 let result = DatasetID::from_str("dataset-123");
3161 assert!(result.is_err());
3162 match result {
3163 Err(Error::InvalidParameters(msg)) => {
3164 assert!(msg.contains("must start with 'ds-'"));
3165 }
3166 _ => panic!("Expected InvalidParameters error"),
3167 }
3168 }
3169
3170 #[test]
3171 fn test_dataset_id_from_str_invalid_hex() {
3172 let result = DatasetID::from_str("ds-zzz");
3173 assert!(result.is_err());
3174 }
3175
3176 #[test]
3177 fn test_dataset_id_into_u64() {
3178 let id = DatasetID::from(111111);
3179 let value: u64 = id.into();
3180 assert_eq!(value, 111111);
3181 }
3182
3183 #[test]
3185 fn test_annotation_set_id_from_u64() {
3186 let id = AnnotationSetID::from(222333);
3187 assert_eq!(id.value(), 222333);
3188 }
3189
3190 #[test]
3191 fn test_annotation_set_id_display() {
3192 let id = AnnotationSetID::from(0xabcdef);
3193 assert_eq!(format!("{}", id), "as-abcdef");
3194 }
3195
3196 #[test]
3197 fn test_annotation_set_id_from_str_valid() {
3198 let id = AnnotationSetID::from_str("as-abcdef").unwrap();
3199 assert_eq!(id.value(), 0xabcdef);
3200 }
3201
3202 #[test]
3203 fn test_annotation_set_id_try_from_str_valid() {
3204 let id = AnnotationSetID::try_from("as-123456").unwrap();
3205 assert_eq!(id.value(), 0x123456);
3206 }
3207
3208 #[test]
3209 fn test_annotation_set_id_try_from_string_valid() {
3210 let id = AnnotationSetID::try_from("as-fedcba".to_string()).unwrap();
3211 assert_eq!(id.value(), 0xfedcba);
3212 }
3213
3214 #[test]
3215 fn test_annotation_set_id_from_str_invalid_prefix() {
3216 let result = AnnotationSetID::from_str("annotation-123");
3217 assert!(result.is_err());
3218 match result {
3219 Err(Error::InvalidParameters(msg)) => {
3220 assert!(msg.contains("must start with 'as-'"));
3221 }
3222 _ => panic!("Expected InvalidParameters error"),
3223 }
3224 }
3225
3226 #[test]
3227 fn test_annotation_set_id_from_str_invalid_hex() {
3228 let result = AnnotationSetID::from_str("as-zzz");
3229 assert!(result.is_err());
3230 }
3231
3232 #[test]
3233 fn test_annotation_set_id_into_u64() {
3234 let id = AnnotationSetID::from(444555);
3235 let value: u64 = id.into();
3236 assert_eq!(value, 444555);
3237 }
3238
3239 #[test]
3241 fn test_sample_id_from_u64() {
3242 let id = SampleID::from(666777);
3243 assert_eq!(id.value(), 666777);
3244 }
3245
3246 #[test]
3247 fn test_sample_id_display() {
3248 let id = SampleID::from(0x987654);
3249 assert_eq!(format!("{}", id), "s-987654");
3250 }
3251
3252 #[test]
3253 fn test_sample_id_try_from_str_valid() {
3254 let id = SampleID::try_from("s-987654").unwrap();
3255 assert_eq!(id.value(), 0x987654);
3256 }
3257
3258 #[test]
3259 fn test_sample_id_try_from_str_invalid_prefix() {
3260 let result = SampleID::try_from("sample-123");
3261 assert!(result.is_err());
3262 match result {
3263 Err(Error::InvalidParameters(msg)) => {
3264 assert!(msg.contains("must start with 's-'"));
3265 }
3266 _ => panic!("Expected InvalidParameters error"),
3267 }
3268 }
3269
3270 #[test]
3271 fn test_sample_id_try_from_str_invalid_hex() {
3272 let result = SampleID::try_from("s-zzz");
3273 assert!(result.is_err());
3274 }
3275
3276 #[test]
3277 fn test_sample_id_into_u64() {
3278 let id = SampleID::from(888999);
3279 let value: u64 = id.into();
3280 assert_eq!(value, 888999);
3281 }
3282
3283 #[test]
3285 fn test_app_id_from_u64() {
3286 let id = AppId::from(123123);
3287 assert_eq!(id.value(), 123123);
3288 }
3289
3290 #[test]
3291 fn test_app_id_display() {
3292 let id = AppId::from(0x456789);
3293 assert_eq!(format!("{}", id), "app-456789");
3294 }
3295
3296 #[test]
3297 fn test_app_id_try_from_str_valid() {
3298 let id = AppId::try_from("app-456789").unwrap();
3299 assert_eq!(id.value(), 0x456789);
3300 }
3301
3302 #[test]
3303 fn test_app_id_try_from_str_invalid_prefix() {
3304 let result = AppId::try_from("application-123");
3305 assert!(result.is_err());
3306 match result {
3307 Err(Error::InvalidParameters(msg)) => {
3308 assert!(msg.contains("must start with 'app-'"));
3309 }
3310 _ => panic!("Expected InvalidParameters error"),
3311 }
3312 }
3313
3314 #[test]
3315 fn test_app_id_try_from_str_invalid_hex() {
3316 let result = AppId::try_from("app-zzz");
3317 assert!(result.is_err());
3318 }
3319
3320 #[test]
3321 fn test_app_id_into_u64() {
3322 let id = AppId::from(321321);
3323 let value: u64 = id.into();
3324 assert_eq!(value, 321321);
3325 }
3326
3327 #[test]
3329 fn test_image_id_from_u64() {
3330 let id = ImageId::from(789789);
3331 assert_eq!(id.value(), 789789);
3332 }
3333
3334 #[test]
3335 fn test_image_id_display() {
3336 let id = ImageId::from(0xabcd1234);
3337 assert_eq!(format!("{}", id), "im-abcd1234");
3338 }
3339
3340 #[test]
3341 fn test_image_id_try_from_str_valid() {
3342 let id = ImageId::try_from("im-abcd1234").unwrap();
3343 assert_eq!(id.value(), 0xabcd1234);
3344 }
3345
3346 #[test]
3347 fn test_image_id_try_from_str_invalid_prefix() {
3348 let result = ImageId::try_from("image-123");
3349 assert!(result.is_err());
3350 match result {
3351 Err(Error::InvalidParameters(msg)) => {
3352 assert!(msg.contains("must start with 'im-'"));
3353 }
3354 _ => panic!("Expected InvalidParameters error"),
3355 }
3356 }
3357
3358 #[test]
3359 fn test_image_id_try_from_str_invalid_hex() {
3360 let result = ImageId::try_from("im-zzz");
3361 assert!(result.is_err());
3362 }
3363
3364 #[test]
3365 fn test_image_id_into_u64() {
3366 let id = ImageId::from(987987);
3367 let value: u64 = id.into();
3368 assert_eq!(value, 987987);
3369 }
3370
3371 #[test]
3373 fn test_id_types_equality() {
3374 let id1 = ProjectID::from(12345);
3375 let id2 = ProjectID::from(12345);
3376 let id3 = ProjectID::from(54321);
3377
3378 assert_eq!(id1, id2);
3379 assert_ne!(id1, id3);
3380 }
3381
3382 #[test]
3383 fn test_id_types_hash() {
3384 use std::collections::HashSet;
3385
3386 let mut set = HashSet::new();
3387 set.insert(DatasetID::from(100));
3388 set.insert(DatasetID::from(200));
3389 set.insert(DatasetID::from(100)); assert_eq!(set.len(), 2);
3392 assert!(set.contains(&DatasetID::from(100)));
3393 assert!(set.contains(&DatasetID::from(200)));
3394 }
3395
3396 #[test]
3397 fn test_id_types_copy_clone() {
3398 let id1 = ExperimentID::from(999);
3399 let id2 = id1; let id3 = id1; assert_eq!(id1, id2);
3403 assert_eq!(id1, id3);
3404 }
3405
3406 #[test]
3408 fn test_id_zero_value() {
3409 let id = ProjectID::from(0);
3410 assert_eq!(format!("{}", id), "p-0");
3411 assert_eq!(id.value(), 0);
3412 }
3413
3414 #[test]
3415 fn test_id_max_value() {
3416 let id = ProjectID::from(u64::MAX);
3417 assert_eq!(format!("{}", id), "p-ffffffffffffffff");
3418 assert_eq!(id.value(), u64::MAX);
3419 }
3420
3421 #[test]
3422 fn test_id_round_trip_conversion() {
3423 let original = 0xdeadbeef_u64;
3424 let id = TrainingSessionID::from(original);
3425 let back: u64 = id.into();
3426 assert_eq!(original, back);
3427 }
3428
3429 #[test]
3430 fn test_id_case_insensitive_hex() {
3431 let id1 = DatasetID::from_str("ds-ABCDEF").unwrap();
3433 let id2 = DatasetID::from_str("ds-abcdef").unwrap();
3434 assert_eq!(id1.value(), id2.value());
3435 }
3436
3437 #[test]
3438 fn test_id_with_leading_zeros() {
3439 let id = ProjectID::from_str("p-00001234").unwrap();
3440 assert_eq!(id.value(), 0x1234);
3441 }
3442
3443 #[test]
3445 fn test_parameter_integer() {
3446 let param = Parameter::Integer(42);
3447 match param {
3448 Parameter::Integer(val) => assert_eq!(val, 42),
3449 _ => panic!("Expected Integer variant"),
3450 }
3451 }
3452
3453 #[test]
3454 fn test_parameter_real() {
3455 let param = Parameter::Real(2.5);
3456 match param {
3457 Parameter::Real(val) => assert_eq!(val, 2.5),
3458 _ => panic!("Expected Real variant"),
3459 }
3460 }
3461
3462 #[test]
3463 fn test_parameter_boolean() {
3464 let param = Parameter::Boolean(true);
3465 match param {
3466 Parameter::Boolean(val) => assert!(val),
3467 _ => panic!("Expected Boolean variant"),
3468 }
3469 }
3470
3471 #[test]
3472 fn test_parameter_string() {
3473 let param = Parameter::String("test".to_string());
3474 match param {
3475 Parameter::String(val) => assert_eq!(val, "test"),
3476 _ => panic!("Expected String variant"),
3477 }
3478 }
3479
3480 #[test]
3481 fn test_parameter_array() {
3482 let param = Parameter::Array(vec![
3483 Parameter::Integer(1),
3484 Parameter::Integer(2),
3485 Parameter::Integer(3),
3486 ]);
3487 match param {
3488 Parameter::Array(arr) => assert_eq!(arr.len(), 3),
3489 _ => panic!("Expected Array variant"),
3490 }
3491 }
3492
3493 #[test]
3494 fn test_parameter_object() {
3495 let mut map = HashMap::new();
3496 map.insert("key".to_string(), Parameter::Integer(100));
3497 let param = Parameter::Object(map);
3498 match param {
3499 Parameter::Object(obj) => {
3500 assert_eq!(obj.len(), 1);
3501 assert!(obj.contains_key("key"));
3502 }
3503 _ => panic!("Expected Object variant"),
3504 }
3505 }
3506
3507 #[test]
3508 fn test_parameter_clone() {
3509 let param1 = Parameter::Integer(42);
3510 let param2 = param1.clone();
3511 assert_eq!(param1, param2);
3512 }
3513
3514 #[test]
3515 fn test_parameter_nested() {
3516 let inner_array = Parameter::Array(vec![Parameter::Integer(1), Parameter::Integer(2)]);
3517 let outer_array = Parameter::Array(vec![inner_array.clone(), inner_array]);
3518
3519 match outer_array {
3520 Parameter::Array(arr) => {
3521 assert_eq!(arr.len(), 2);
3522 }
3523 _ => panic!("Expected Array variant"),
3524 }
3525 }
3526
3527 macro_rules! test_typeid_conversions {
3530 ($test_name:ident, $type:ty, $prefix:literal, $wrong_prefix:literal) => {
3531 #[test]
3532 fn $test_name() {
3533 let id = <$type>::from(0xabc123);
3535 assert_eq!(id.value(), 0xabc123);
3536
3537 assert_eq!(format!("{}", id), concat!($prefix, "-abc123"));
3539
3540 let id: $type = concat!($prefix, "-abc123").parse().unwrap();
3542 assert_eq!(id.value(), 0xabc123);
3543
3544 assert!(concat!($wrong_prefix, "-abc").parse::<$type>().is_err());
3546
3547 assert!("abc123".parse::<$type>().is_err());
3549
3550 assert!(concat!($prefix, "-xyz").parse::<$type>().is_err());
3552
3553 let id = <$type>::try_from(concat!($prefix, "-abc123")).unwrap();
3555 assert_eq!(id.value(), 0xabc123);
3556
3557 let id = <$type>::try_from(concat!($prefix, "-abc123").to_string()).unwrap();
3559 assert_eq!(id.value(), 0xabc123);
3560
3561 let id = <$type>::from(0xabc123);
3563 let json = serde_json::to_string(&id).unwrap();
3564 let parsed: $type = serde_json::from_str(&json).unwrap();
3565 assert_eq!(id, parsed);
3566
3567 let id = <$type>::from(0xabc123);
3569 let val: u64 = id.into();
3570 assert_eq!(val, 0xabc123);
3571 }
3572 };
3573 }
3574
3575 test_typeid_conversions!(test_organization_id_conversions, OrganizationID, "org", "p");
3576 test_typeid_conversions!(test_project_id_conversions, ProjectID, "p", "org");
3577 test_typeid_conversions!(test_experiment_id_conversions, ExperimentID, "exp", "p");
3578 test_typeid_conversions!(
3579 test_training_session_id_conversions,
3580 TrainingSessionID,
3581 "t",
3582 "v"
3583 );
3584 test_typeid_conversions!(
3585 test_validation_session_id_conversions,
3586 ValidationSessionID,
3587 "v",
3588 "t"
3589 );
3590 test_typeid_conversions!(test_snapshot_id_conversions, SnapshotID, "ss", "ds");
3591 test_typeid_conversions!(test_task_id_conversions, TaskID, "task", "t");
3592 test_typeid_conversions!(test_dataset_id_conversions, DatasetID, "ds", "ss");
3593 test_typeid_conversions!(
3594 test_annotation_set_id_conversions,
3595 AnnotationSetID,
3596 "as",
3597 "ds"
3598 );
3599 test_typeid_conversions!(test_sample_id_conversions, SampleID, "s", "p");
3600 test_typeid_conversions!(test_app_id_conversions, AppId, "app", "p");
3601 test_typeid_conversions!(test_image_id_conversions, ImageId, "im", "se");
3602 test_typeid_conversions!(test_sequence_id_conversions, SequenceId, "se", "im");
3603
3604 #[test]
3607 fn test_version_tag_deserialize_full() {
3608 let json = r#"{
3609 "id": 456, "dataset_id": 1715004, "name": "training-v1.0",
3610 "serial": 42, "description": "Ready for production",
3611 "created_by": "user@example.com", "created_at": "2025-01-15T10:30:00Z",
3612 "image_count": 50000, "annotation_counts": {"box": 150000, "seg": 20000},
3613 "sensor_counts": {"lidar": 25000}, "label_count": 15,
3614 "annotation_set_count": 3, "snapshot_id": 789
3615 }"#;
3616 let tag: VersionTag = serde_json::from_str(json).unwrap();
3617 assert_eq!(tag.name(), "training-v1.0");
3618 assert_eq!(tag.serial(), 42);
3619 assert_eq!(tag.image_count(), 50000);
3620 assert_eq!(tag.annotation_counts().get("box"), Some(&150000));
3621 assert_eq!(tag.snapshot_id(), Some(789));
3622 }
3623
3624 #[test]
3625 fn test_version_tag_deserialize_omitempty() {
3626 let json = r#"{
3628 "id": 1, "dataset_id": 2, "name": "v1.0", "serial": 5,
3629 "description": "", "created_by": "user",
3630 "created_at": "2025-01-01T00:00:00Z"
3631 }"#;
3632 let tag: VersionTag = serde_json::from_str(json).unwrap();
3633 assert_eq!(tag.snapshot_id(), None);
3634 assert_eq!(tag.image_count(), 0);
3635 assert!(tag.annotation_counts().is_empty());
3636 }
3637
3638 #[test]
3639 fn test_changelog_entry_deserialize_omitempty() {
3640 let json = r#"{
3642 "id": 1, "dataset_id": 2, "serial": 3, "entity_type": "image",
3643 "operation": "bulk_create", "change_data": {"count": 5},
3644 "username": "user", "organization_id": 1,
3645 "created_at": "2025-01-01T00:00:00Z", "message": ""
3646 }"#;
3647 let entry: ChangelogEntry = serde_json::from_str(json).unwrap();
3648 assert!(entry.entity_id().is_none());
3649 assert!(entry.s3_version_ids().is_empty());
3650 assert_eq!(entry.entity_type(), "image");
3651 assert_eq!(entry.operation(), "bulk_create");
3652 }
3653
3654 #[test]
3655 fn test_changelog_response_deserialize() {
3656 let json = r#"{
3657 "entries": [], "count": 0, "continue_token": ""
3658 }"#;
3659 let resp: ChangelogResponse = serde_json::from_str(json).unwrap();
3660 assert!(resp.entries.is_empty());
3661 assert_eq!(resp.count, 0);
3662 assert!(resp.continue_token.is_empty());
3663 assert!(resp.from_serial.is_none());
3664 }
3665
3666 #[test]
3667 fn test_version_current_no_latest_tag() {
3668 let json = r#"{
3670 "dataset_id": 100, "current_serial": 5, "tags": []
3671 }"#;
3672 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3673 assert!(resp.latest_tag.is_none());
3674 assert!(resp.tags.is_empty());
3675 assert_eq!(resp.current_serial, 5);
3676 }
3677
3678 #[test]
3679 fn test_version_current_with_latest_tag() {
3680 let json = r#"{
3681 "dataset_id": 100, "current_serial": 42,
3682 "latest_tag": {
3683 "id": 1, "dataset_id": 100, "name": "v1.0", "serial": 42,
3684 "description": "test", "created_by": "user",
3685 "created_at": "2025-01-01T00:00:00Z",
3686 "image_count": 10, "label_count": 2, "annotation_set_count": 1
3687 },
3688 "tags": []
3689 }"#;
3690 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3691 assert!(resp.latest_tag.is_some());
3692 assert_eq!(resp.latest_tag.unwrap().name(), "v1.0");
3693 }
3694
3695 #[test]
3696 fn test_version_tag_is_current_field() {
3697 let json = r#"{
3698 "id": 1, "dataset_id": 5, "name": "v1.0", "serial": 10,
3699 "created_by": "alice", "created_at": "2026-01-01T00:00:00Z",
3700 "is_current": true
3701 }"#;
3702 let tag: VersionTag = serde_json::from_str(json).unwrap();
3703 assert!(tag.is_current());
3704 }
3705
3706 #[test]
3707 fn test_version_tag_is_current_false() {
3708 let json = r#"{
3709 "id": 1, "dataset_id": 5, "name": "v1.0", "serial": 10,
3710 "created_by": "alice", "created_at": "2026-01-01T00:00:00Z",
3711 "is_current": false
3712 }"#;
3713 let tag: VersionTag = serde_json::from_str(json).unwrap();
3714 assert!(!tag.is_current());
3715 }
3716
3717 #[test]
3718 fn test_dataset_summary_deserialize() {
3719 let json = r#"{
3720 "dataset_id": 100, "current_serial": 10,
3721 "image_count": 5000, "annotation_counts": {"box": 10000},
3722 "sensor_counts": {}, "label_count": 8,
3723 "annotation_set_count": 2, "last_updated": "2025-06-01T12:00:00Z"
3724 }"#;
3725 let summary: DatasetSummary = serde_json::from_str(json).unwrap();
3726 assert_eq!(summary.image_count(), 5000);
3727 assert_eq!(summary.label_count(), 8);
3728 assert_eq!(summary.annotation_counts().get("box"), Some(&10000));
3729 }
3730
3731 #[test]
3732 fn test_restore_result_deserialize() {
3733 let json = r#"{
3734 "success": true, "new_serial": 45,
3735 "restored_from": {"tag": "v1.0", "serial": 42},
3736 "restored_counts": {"images": 5000, "labels": 15, "annotation_sets": 3},
3737 "message": "Dataset restored to tag v1.0"
3738 }"#;
3739 let result: RestoreResult = serde_json::from_str(json).unwrap();
3740 assert!(result.success);
3741 assert_eq!(result.new_serial, 45);
3742 assert_eq!(result.restored_from.tag, "v1.0");
3743 assert_eq!(result.restored_from.serial, 42);
3744 assert_eq!(result.restored_counts.images, 5000);
3745 }
3746}
3747
3748#[cfg(test)]
3749mod tests_task_data_list {
3750 use super::*;
3751
3752 #[test]
3753 fn task_data_list_deserializes_from_server_shape() {
3754 let json = r#"{
3755 "server": "test.edgefirst.studio",
3756 "organization_uid": "org-abc123",
3757 "traces": ["trace/imx95.json"],
3758 "data": {
3759 "predictions": ["predictions.parquet"],
3760 "trace": ["imx95.json"]
3761 }
3762 }"#;
3763 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
3764 assert_eq!(parsed.server, "test.edgefirst.studio");
3765 assert_eq!(parsed.organization_uid, "org-abc123");
3766 assert_eq!(parsed.traces, vec!["trace/imx95.json"]);
3767 assert_eq!(
3768 parsed.data.get("predictions").unwrap(),
3769 &vec!["predictions.parquet".to_string()]
3770 );
3771 }
3772}
3773
3774#[cfg(test)]
3775mod tests_upload_data {
3776 #[test]
3780 fn folder_empty_string_is_normalised() {
3781 let folder: Option<&str> = Some("");
3782 assert!(folder.filter(|s| !s.is_empty()).is_none());
3783
3784 let folder_real: Option<&str> = Some("predictions");
3785 assert!(folder_real.filter(|s| !s.is_empty()).is_some());
3786 }
3787}
3788
3789#[cfg(test)]
3790mod tests_job_struct {
3791 use super::*;
3792
3793 #[test]
3794 fn job_deserializes_with_all_fields() {
3795 let json = r#"{
3796 "code": "edgefirst-validator:2.9.5",
3797 "title": "EdgeFirst Validator",
3798 "job_name": "smoke-test",
3799 "job_id": "aws-batch-abc",
3800 "state": "RUNNING",
3801 "launch": "2026-05-14T15:00:00Z",
3802 "task_id": 6789
3803 }"#;
3804 let job: Job = serde_json::from_str(json).unwrap();
3805 assert_eq!(job.code, "edgefirst-validator:2.9.5");
3806 assert_eq!(job.title, "EdgeFirst Validator");
3807 assert_eq!(job.job_name, "smoke-test");
3808 assert_eq!(job.job_id, "aws-batch-abc");
3809 assert_eq!(job.state, "RUNNING");
3810 assert!(job.launch.is_some());
3811 assert_eq!(job.task_id, 6789);
3812 }
3813
3814 #[test]
3815 fn job_tolerates_missing_optional_fields() {
3816 let json = r#"{ "task_id": 42 }"#;
3820 let job: Job = serde_json::from_str(json).unwrap();
3821 assert_eq!(job.task_id, 42);
3822 assert!(job.code.is_empty());
3823 assert!(job.title.is_empty());
3824 assert!(job.job_name.is_empty());
3825 assert!(job.job_id.is_empty());
3826 assert!(job.state.is_empty());
3827 assert!(job.launch.is_none());
3828 }
3829
3830 #[test]
3831 fn job_task_id_accessor_saturates_negative_to_zero() {
3832 let job = Job {
3837 code: String::new(),
3838 title: String::new(),
3839 job_name: String::new(),
3840 job_id: String::new(),
3841 state: String::new(),
3842 launch: None,
3843 task_id: -1,
3844 };
3845 assert_eq!(job.task_id().value(), 0);
3846 }
3847
3848 #[test]
3849 fn job_task_id_accessor_passes_through_positive_values() {
3850 let job = Job {
3851 code: String::new(),
3852 title: String::new(),
3853 job_name: String::new(),
3854 job_id: String::new(),
3855 state: String::new(),
3856 launch: None,
3857 task_id: 12345,
3858 };
3859 assert_eq!(job.task_id().value(), 12345);
3860 }
3861
3862 #[test]
3863 fn job_ignores_unknown_fields() {
3864 let json = r#"{
3868 "code": "x",
3869 "task_id": 1,
3870 "docker_task": { "image": "x" },
3871 "aws_region": "us-east-1",
3872 "tags": ["a", "b"]
3873 }"#;
3874 let job: Job = serde_json::from_str(json).unwrap();
3875 assert_eq!(job.task_id, 1);
3876 }
3877}
3878
3879#[cfg(test)]
3880mod tests_task_info_schema_tolerance {
3881 use super::*;
3882
3883 #[test]
3888 fn task_info_accepts_task_description_field() {
3889 let json = r#"{
3891 "id": 6699,
3892 "type": "edgefirst-validator:2.9.5",
3893 "task_description": "Profiler run for IMX95",
3894 "status": "running"
3895 }"#;
3896 let info: TaskInfo = serde_json::from_str(json).unwrap();
3897 assert_eq!(info.description(), "Profiler run for IMX95");
3898 }
3899
3900 #[test]
3901 fn task_info_accepts_legacy_description_field() {
3902 let json = r#"{
3904 "id": 6699,
3905 "type": "edgefirst-validator:2.9.5",
3906 "description": "Legacy description"
3907 }"#;
3908 let info: TaskInfo = serde_json::from_str(json).unwrap();
3909 assert_eq!(info.description(), "Legacy description");
3910 }
3911
3912 #[test]
3913 fn task_info_tolerates_missing_description() {
3914 let json = r#"{
3916 "id": 6699,
3917 "type": "x"
3918 }"#;
3919 let info: TaskInfo = serde_json::from_str(json).unwrap();
3920 assert!(info.description().is_empty());
3921 }
3922
3923 #[test]
3924 fn task_info_tolerates_missing_dates_via_default() {
3925 let json = r#"{
3927 "id": 6699,
3928 "type": "x"
3929 }"#;
3930 let info: TaskInfo = serde_json::from_str(json).unwrap();
3931 assert_eq!(info.id().value(), 6699);
3933 }
3934
3935 #[test]
3936 fn task_info_status_accessor_returns_option() {
3937 let json = r#"{
3938 "id": 1,
3939 "type": "x"
3940 }"#;
3941 let info: TaskInfo = serde_json::from_str(json).unwrap();
3942 assert!(info.status().is_none());
3943 }
3944
3945 #[test]
3946 fn task_info_stages_returns_empty_map_when_unset() {
3947 let json = r#"{
3948 "id": 1,
3949 "type": "x"
3950 }"#;
3951 let info: TaskInfo = serde_json::from_str(json).unwrap();
3952 let stages = info.stages();
3953 assert!(stages.is_empty());
3954 }
3955}
3956
3957#[cfg(test)]
3958mod tests_stage_struct {
3959 use super::*;
3960
3961 #[test]
3962 fn stage_new_sets_only_supplied_fields() {
3963 let stage = Stage::new(
3964 None,
3965 "download".into(),
3966 Some("running".into()),
3967 Some("fetching".into()),
3968 42,
3969 );
3970 assert!(stage.task_id().is_none());
3971 assert_eq!(stage.stage(), "download");
3972 assert_eq!(stage.status().as_deref(), Some("running"));
3973 assert_eq!(stage.message().as_deref(), Some("fetching"));
3974 assert_eq!(stage.percentage(), 42);
3975 assert!(stage.description().is_none());
3977 }
3978
3979 #[test]
3980 fn stage_serializes_without_optional_none_fields() {
3981 let stage = Stage::new(None, "init".into(), None, None, 0);
3983 let json = serde_json::to_value(&stage).unwrap();
3984 assert!(json.get("status").is_none(), "got: {json}");
3985 assert!(json.get("message").is_none(), "got: {json}");
3986 assert!(json.get("docker_task_id").is_none(), "got: {json}");
3987 assert_eq!(json["stage"], "init");
3989 assert_eq!(json["percentage"], 0);
3990 }
3991
3992 #[test]
3993 fn stage_serializes_task_id_when_present() {
3994 let task_id = TaskID::from(0xdeadu64);
3995 let stage = Stage::new(Some(task_id), "x".into(), None, None, 0);
3996 let json = serde_json::to_value(&stage).unwrap();
3997 assert!(json.get("docker_task_id").is_some());
4000 }
4001
4002 #[test]
4003 fn stage_round_trips_through_json() {
4004 let stage = Stage::new(
4005 None,
4006 "train".into(),
4007 Some("done".into()),
4008 Some("epoch 100".into()),
4009 100,
4010 );
4011 let s = serde_json::to_string(&stage).unwrap();
4012 let back: Stage = serde_json::from_str(&s).unwrap();
4013 assert_eq!(back.stage(), "train");
4014 assert_eq!(back.status().as_deref(), Some("done"));
4015 assert_eq!(back.message().as_deref(), Some("epoch 100"));
4016 assert_eq!(back.percentage(), 100);
4017 }
4018}
4019
4020#[cfg(test)]
4021mod tests_task_data_list_extra {
4022 use super::*;
4023
4024 #[test]
4025 fn task_data_list_with_empty_data_map() {
4026 let json = r#"{
4027 "server": "studio",
4028 "organization_uid": "org-1",
4029 "traces": [],
4030 "data": {}
4031 }"#;
4032 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4033 assert!(parsed.traces.is_empty());
4034 assert!(parsed.data.is_empty());
4035 }
4036
4037 #[test]
4038 fn task_data_list_multiple_folders() {
4039 let json = r#"{
4040 "server": "studio",
4041 "organization_uid": "org-1",
4042 "traces": ["t1", "t2"],
4043 "data": {
4044 "predictions": ["a.parquet", "b.parquet"],
4045 "metrics": ["loss.json"]
4046 }
4047 }"#;
4048 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4049 assert_eq!(parsed.traces.len(), 2);
4050 assert_eq!(parsed.data.len(), 2);
4051 assert_eq!(parsed.data["predictions"].len(), 2);
4052 }
4053}
4054
4055#[cfg(test)]
4056mod tests_artifact_struct {
4057 use super::*;
4058
4059 #[test]
4060 fn artifact_accessors_return_strs() {
4061 let json = r#"{ "name": "best.onnx", "modelType": "yolo" }"#;
4064 let a: Artifact = serde_json::from_str(json).unwrap();
4065 assert_eq!(a.name(), "best.onnx");
4066 assert_eq!(a.model_type(), "yolo");
4067 }
4068}
4069
4070#[cfg(test)]
4071mod tests_task_status_serialize {
4072 use super::*;
4073
4074 #[test]
4075 fn task_status_uses_docker_task_id_wire_field() {
4076 let s = TaskStatus {
4077 task_id: TaskID::from(0x1a2bu64),
4078 status: "training".into(),
4079 };
4080 let json = serde_json::to_value(&s).unwrap();
4081 assert!(json.get("docker_task_id").is_some(), "got: {json}");
4083 assert_eq!(json["status"], "training");
4084 }
4085}
4086
4087#[cfg(test)]
4088mod tests_task_stages_serialize {
4089 use super::*;
4090
4091 #[test]
4092 fn task_stages_omits_empty_vec() {
4093 let stages = TaskStages {
4094 task_id: TaskID::from(1u64),
4095 stages: Vec::new(),
4096 };
4097 let json = serde_json::to_value(&stages).unwrap();
4098 assert!(json.get("stages").is_none(), "got: {json}");
4100 }
4101
4102 #[test]
4103 fn task_stages_serializes_non_empty_vec() {
4104 let stages = TaskStages {
4105 task_id: TaskID::from(1u64),
4106 stages: vec![std::collections::HashMap::from([(
4107 "stage".to_string(),
4108 "download".to_string(),
4109 )])],
4110 };
4111 let json = serde_json::to_value(&stages).unwrap();
4112 assert_eq!(json["stages"][0]["stage"], "download");
4113 }
4114}