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>,
1473 pub train_group: Option<String>,
1475 pub val_group: Option<String>,
1477 pub session_name: Option<String>,
1480 pub session_description: Option<String>,
1482 pub weights_session: Option<TrainingSessionID>,
1484 pub params: HashMap<String, Parameter>,
1486 pub is_local: bool,
1488 pub is_kubernetes: bool,
1490}
1491
1492#[derive(Deserialize, Debug, Clone)]
1505pub struct NewTrainingSession {
1506 #[serde(rename = "id")]
1507 pub task_id: TaskID,
1508 #[serde(rename = "train_session_id", default)]
1509 pub session_id: Option<TrainingSessionID>,
1510}
1511
1512impl Display for NewTrainingSession {
1513 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1514 match self.session_id {
1515 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1516 None => write!(f, "task {} (no session)", self.task_id),
1517 }
1518 }
1519}
1520
1521#[derive(Deserialize, Debug, Clone)]
1526pub struct Tag {
1527 pub id: u64,
1529 pub name: String,
1531 #[serde(default)]
1533 pub dataset_id: u64,
1534}
1535
1536#[derive(Deserialize, Clone, Debug)]
1537pub struct DatasetParams {
1538 dataset_id: DatasetID,
1539 annotation_set_id: AnnotationSetID,
1540 #[serde(rename = "train_group_name")]
1541 train_group: String,
1542 #[serde(rename = "val_group_name")]
1543 val_group: String,
1544}
1545
1546impl DatasetParams {
1547 pub fn dataset_id(&self) -> DatasetID {
1548 self.dataset_id
1549 }
1550
1551 pub fn annotation_set_id(&self) -> AnnotationSetID {
1552 self.annotation_set_id
1553 }
1554
1555 pub fn train_group(&self) -> &str {
1556 &self.train_group
1557 }
1558
1559 pub fn val_group(&self) -> &str {
1560 &self.val_group
1561 }
1562}
1563
1564#[derive(Serialize, Debug, Clone)]
1565pub struct TasksListParams {
1566 #[serde(skip_serializing_if = "Option::is_none")]
1567 pub continue_token: Option<String>,
1568 #[serde(skip_serializing_if = "Option::is_none")]
1569 pub types: Option<Vec<String>>,
1570 #[serde(rename = "manage_types", skip_serializing_if = "Option::is_none")]
1571 pub manager: Option<Vec<String>>,
1572 #[serde(skip_serializing_if = "Option::is_none")]
1573 pub status: Option<Vec<String>>,
1574}
1575
1576#[derive(Debug, Clone, Serialize, Deserialize)]
1582pub struct TaskDataList {
1583 pub server: String,
1584 #[serde(rename = "organization_uid")]
1585 pub organization_uid: String,
1586 #[serde(default)]
1587 pub traces: Vec<String>,
1588 #[serde(default)]
1589 pub data: std::collections::HashMap<String, Vec<String>>,
1590}
1591
1592#[derive(Debug, Clone, Serialize, Deserialize)]
1597pub struct Job {
1598 #[serde(default)]
1600 pub code: String,
1601 #[serde(default)]
1603 pub title: String,
1604 #[serde(default)]
1606 pub job_name: String,
1607 #[serde(default)]
1609 pub job_id: String,
1610 #[serde(default)]
1612 pub state: String,
1613 #[serde(default)]
1615 pub launch: Option<DateTime<Utc>>,
1616 pub task_id: i64,
1621}
1622
1623impl Job {
1624 pub fn task_id(&self) -> TaskID {
1630 TaskID::from(self.task_id.max(0) as u64)
1631 }
1632}
1633
1634#[derive(Deserialize, Debug, Clone)]
1635pub struct TasksListResult {
1636 pub tasks: Vec<Task>,
1637 pub continue_token: Option<String>,
1638}
1639
1640#[derive(Deserialize, Debug, Clone)]
1641pub struct Task {
1642 id: TaskID,
1643 name: String,
1644 #[serde(rename = "type")]
1645 workflow: String,
1646 status: String,
1647 #[serde(rename = "manage_type")]
1648 manager: Option<String>,
1649 #[serde(rename = "instance_type")]
1650 instance: String,
1651 #[serde(rename = "date")]
1652 created: DateTime<Utc>,
1653}
1654
1655impl Task {
1656 pub fn id(&self) -> TaskID {
1657 self.id
1658 }
1659
1660 pub fn name(&self) -> &str {
1661 &self.name
1662 }
1663
1664 pub fn workflow(&self) -> &str {
1665 &self.workflow
1666 }
1667
1668 pub fn status(&self) -> &str {
1669 &self.status
1670 }
1671
1672 pub fn manager(&self) -> Option<&str> {
1673 self.manager.as_deref()
1674 }
1675
1676 pub fn instance(&self) -> &str {
1677 &self.instance
1678 }
1679
1680 pub fn created(&self) -> &DateTime<Utc> {
1681 &self.created
1682 }
1683}
1684
1685impl Display for Task {
1686 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1687 write!(
1688 f,
1689 "{} [{:?} {}] {}",
1690 self.id,
1691 self.manager(),
1692 self.workflow(),
1693 self.name()
1694 )
1695 }
1696}
1697
1698#[derive(Deserialize, Debug, Clone)]
1699pub struct TaskInfo {
1700 id: TaskID,
1701 project_id: Option<ProjectID>,
1702 #[serde(rename = "task_description", alias = "description", default)]
1703 description: String,
1704 #[serde(rename = "type")]
1705 workflow: String,
1706 status: Option<String>,
1707 #[serde(default)]
1708 progress: TaskProgress,
1709 #[serde(
1710 rename = "created_date",
1711 alias = "created",
1712 default = "default_datetime_utc"
1713 )]
1714 created: DateTime<Utc>,
1715 #[serde(
1716 rename = "end_date",
1717 alias = "completed",
1718 default = "default_datetime_utc"
1719 )]
1720 completed: DateTime<Utc>,
1721}
1722
1723fn default_datetime_utc() -> DateTime<Utc> {
1724 DateTime::UNIX_EPOCH
1725}
1726
1727impl Display for TaskInfo {
1728 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1729 write!(f, "{} {}: {}", self.id, self.workflow(), self.description())
1730 }
1731}
1732
1733impl TaskInfo {
1734 pub fn id(&self) -> TaskID {
1735 self.id
1736 }
1737
1738 pub fn project_id(&self) -> Option<ProjectID> {
1739 self.project_id
1740 }
1741
1742 pub fn description(&self) -> &str {
1743 &self.description
1744 }
1745
1746 pub fn workflow(&self) -> &str {
1747 &self.workflow
1748 }
1749
1750 pub fn status(&self) -> &Option<String> {
1751 &self.status
1752 }
1753
1754 pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1755 let t = client.task_status(self.id(), status).await?;
1756 self.status = Some(t.status);
1757 Ok(())
1758 }
1759
1760 pub fn stages(&self) -> HashMap<String, Stage> {
1761 match &self.progress.stages {
1762 Some(stages) => stages.clone(),
1763 None => HashMap::new(),
1764 }
1765 }
1766
1767 pub async fn update_stage(
1768 &mut self,
1769 client: &Client,
1770 stage: &str,
1771 status: &str,
1772 message: &str,
1773 percentage: u8,
1774 ) -> Result<(), Error> {
1775 client
1776 .update_stage(self.id(), stage, status, message, percentage)
1777 .await?;
1778 let t = client.task_info(self.id()).await?;
1779 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1780 Ok(())
1781 }
1782
1783 pub async fn set_stages(
1784 &mut self,
1785 client: &Client,
1786 stages: &[(&str, &str)],
1787 ) -> Result<(), Error> {
1788 client.set_stages(self.id(), stages).await?;
1789 let t = client.task_info(self.id()).await?;
1790 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1791 Ok(())
1792 }
1793
1794 pub async fn data_list(&self, client: &client::Client) -> Result<TaskDataList, Error> {
1810 let req = client::TaskDataListRequest {
1811 task_id: self.id().value(),
1812 };
1813 match client.rpc("task.data.list".to_owned(), Some(&req)).await {
1814 Ok(r) => Ok(r),
1815 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1816 "task.data.list",
1817 code,
1818 msg,
1819 Some(self.id()),
1820 )),
1821 Err(e) => Err(e),
1822 }
1823 }
1824
1825 pub async fn upload_data(
1846 &self,
1847 client: &client::Client,
1848 path: &std::path::Path,
1849 folder: Option<&str>,
1850 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1851 ) -> Result<(), Error> {
1852 use futures::StreamExt;
1853 use std::sync::{
1854 Arc,
1855 atomic::{AtomicUsize, Ordering},
1856 };
1857 use tokio_util::io::ReaderStream;
1858
1859 let file_name = path
1860 .file_name()
1861 .and_then(|s| s.to_str())
1862 .ok_or_else(|| Error::InvalidParameters("path must have a UTF-8 filename".into()))?
1863 .to_owned();
1864
1865 let file = tokio::fs::File::open(path).await?;
1866 let total = file.metadata().await?.len() as usize;
1867 let sent = Arc::new(AtomicUsize::new(0));
1868
1869 let reader_stream = ReaderStream::new(file);
1870 let sent_clone = sent.clone();
1871 let progress_clone = progress.clone();
1872 let progress_stream = reader_stream.inspect(move |chunk_result| {
1873 if let Ok(chunk) = chunk_result {
1874 let current = sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1875 if let Some(tx) = &progress_clone {
1881 let _ = tx.try_send(Progress {
1882 current,
1883 total,
1884 status: None,
1885 });
1886 }
1887 }
1888 });
1889
1890 let body = reqwest::Body::wrap_stream(progress_stream);
1891 let file_part = Part::stream_with_length(body, total as u64).file_name(file_name);
1892
1893 let mut form = Form::new().text("task_id", self.id().value().to_string());
1894 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
1895 form = form.text("folder", folder.to_owned());
1896 }
1897 form = form.part("file", file_part);
1898
1899 let result = match client.post_multipart("task.data.upload", form).await {
1900 Ok(_) => Ok(()),
1901 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1902 "task.data.upload",
1903 code,
1904 msg,
1905 Some(self.id()),
1906 )),
1907 Err(e) => Err(e),
1908 };
1909
1910 if result.is_ok()
1914 && let Some(tx) = progress
1915 {
1916 let _ = tx
1917 .send(Progress {
1918 current: total,
1919 total,
1920 status: None,
1921 })
1922 .await;
1923 }
1924 result
1925 }
1926
1927 pub async fn download_data(
1956 &self,
1957 client: &client::Client,
1958 file: &str,
1959 folder: Option<&str>,
1960 output_path: &std::path::Path,
1961 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1962 ) -> Result<(), Error> {
1963 let folder = folder.unwrap_or("").to_owned();
1964 let req = client::TaskDataDownloadRequest {
1965 task_id: self.id().value(),
1966 folder,
1967 file: file.to_owned(),
1968 };
1969 match client
1970 .rpc_download("task.data.download", &req, output_path, progress)
1971 .await
1972 {
1973 Ok(()) => Ok(()),
1974 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1975 "task.data.download",
1976 code,
1977 msg,
1978 Some(self.id()),
1979 )),
1980 Err(e) => Err(e),
1981 }
1982 }
1983
1984 pub async fn add_chart(
2012 &self,
2013 client: &client::Client,
2014 group: &str,
2015 name: &str,
2016 data: Parameter,
2017 params: Option<Parameter>,
2018 ) -> Result<(), Error> {
2019 client::validate_chart_args(group, name)?;
2020 let req = client::TaskChartAddRequest {
2021 task_id: self.id().value(),
2022 group_name: group.to_owned(),
2023 chart_name: name.to_owned(),
2024 params,
2025 data,
2026 };
2027 let _resp: serde_json::Value =
2028 match client.rpc("task.chart.add".to_owned(), Some(&req)).await {
2029 Ok(r) => r,
2030 Err(Error::RpcError(code, msg)) => {
2031 return Err(client::map_rpc_error(
2032 "task.chart.add",
2033 code,
2034 msg,
2035 Some(self.id()),
2036 ));
2037 }
2038 Err(e) => return Err(e),
2039 };
2040 Ok(())
2041 }
2042
2043 pub async fn list_charts(
2060 &self,
2061 client: &client::Client,
2062 group: Option<&str>,
2063 ) -> Result<TaskDataList, Error> {
2064 let req = client::TaskChartListRequest {
2065 task_id: self.id().value(),
2066 group_name: group.unwrap_or("").to_owned(),
2067 };
2068 match client.rpc("task.chart.list".to_owned(), Some(&req)).await {
2069 Ok(r) => Ok(r),
2070 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2071 "task.chart.list",
2072 code,
2073 msg,
2074 Some(self.id()),
2075 )),
2076 Err(e) => Err(e),
2077 }
2078 }
2079
2080 pub async fn get_chart(
2099 &self,
2100 client: &client::Client,
2101 group: &str,
2102 name: &str,
2103 ) -> Result<Parameter, Error> {
2104 client::validate_chart_args(group, name)?;
2105 let req = client::TaskChartGetRequest {
2106 task_id: self.id().value(),
2107 group_name: group.to_owned(),
2108 chart_name: name.to_owned(),
2109 };
2110 match client.rpc("task.chart.get".to_owned(), Some(&req)).await {
2111 Ok(r) => Ok(r),
2112 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2113 "task.chart.get",
2114 code,
2115 msg,
2116 Some(self.id()),
2117 )),
2118 Err(e) => Err(e),
2119 }
2120 }
2121
2122 pub fn created(&self) -> &DateTime<Utc> {
2123 &self.created
2124 }
2125
2126 pub fn completed(&self) -> &DateTime<Utc> {
2127 &self.completed
2128 }
2129}
2130
2131#[derive(Deserialize, Debug, Default, Clone)]
2132pub struct TaskProgress {
2133 stages: Option<HashMap<String, Stage>>,
2134}
2135
2136#[derive(Serialize, Debug, Clone)]
2137pub struct TaskStatus {
2138 #[serde(rename = "docker_task_id")]
2139 pub task_id: TaskID,
2140 pub status: String,
2141}
2142
2143#[derive(Serialize, Deserialize, Debug, Clone)]
2144pub struct Stage {
2145 #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
2146 task_id: Option<TaskID>,
2147 stage: String,
2148 #[serde(skip_serializing_if = "Option::is_none")]
2149 status: Option<String>,
2150 #[serde(skip_serializing_if = "Option::is_none")]
2151 description: Option<String>,
2152 #[serde(skip_serializing_if = "Option::is_none")]
2153 message: Option<String>,
2154 percentage: u8,
2155}
2156
2157impl Stage {
2158 pub fn new(
2159 task_id: Option<TaskID>,
2160 stage: String,
2161 status: Option<String>,
2162 message: Option<String>,
2163 percentage: u8,
2164 ) -> Self {
2165 Stage {
2166 task_id,
2167 stage,
2168 status,
2169 description: None,
2170 message,
2171 percentage,
2172 }
2173 }
2174
2175 pub fn task_id(&self) -> &Option<TaskID> {
2176 &self.task_id
2177 }
2178
2179 pub fn stage(&self) -> &str {
2180 &self.stage
2181 }
2182
2183 pub fn status(&self) -> &Option<String> {
2184 &self.status
2185 }
2186
2187 pub fn description(&self) -> &Option<String> {
2188 &self.description
2189 }
2190
2191 pub fn message(&self) -> &Option<String> {
2192 &self.message
2193 }
2194
2195 pub fn percentage(&self) -> u8 {
2196 self.percentage
2197 }
2198}
2199
2200#[derive(Serialize, Debug)]
2201pub struct TaskStages {
2202 #[serde(rename = "docker_task_id")]
2203 pub task_id: TaskID,
2204 #[serde(skip_serializing_if = "Vec::is_empty")]
2205 pub stages: Vec<HashMap<String, String>>,
2206}
2207
2208#[derive(Deserialize, Debug)]
2209pub struct Artifact {
2210 name: String,
2211 #[serde(rename = "modelType")]
2212 model_type: String,
2213}
2214
2215impl Artifact {
2216 pub fn name(&self) -> &str {
2217 &self.name
2218 }
2219
2220 pub fn model_type(&self) -> &str {
2221 &self.model_type
2222 }
2223}
2224
2225#[derive(Deserialize, Serialize, Clone, Debug)]
2233pub struct VersionTag {
2234 id: u64,
2235 dataset_id: DatasetID,
2236 name: String,
2237 serial: u64,
2238 #[serde(default)]
2239 description: String,
2240 created_by: String,
2241 created_at: DateTime<Utc>,
2242 #[serde(default)]
2243 image_count: u64,
2244 #[serde(default)]
2245 annotation_counts: HashMap<String, u64>,
2246 #[serde(default)]
2247 sensor_counts: HashMap<String, u64>,
2248 #[serde(default)]
2249 label_count: u64,
2250 #[serde(default)]
2251 annotation_set_count: u64,
2252 #[serde(default)]
2253 snapshot_id: Option<u64>,
2254 #[serde(default)]
2255 is_current: bool,
2256}
2257
2258impl VersionTag {
2259 pub fn id(&self) -> u64 {
2261 self.id
2262 }
2263
2264 pub fn dataset_id(&self) -> DatasetID {
2266 self.dataset_id
2267 }
2268
2269 pub fn name(&self) -> &str {
2271 &self.name
2272 }
2273
2274 pub fn serial(&self) -> u64 {
2276 self.serial
2277 }
2278
2279 pub fn description(&self) -> &str {
2281 &self.description
2282 }
2283
2284 pub fn created_by(&self) -> &str {
2286 &self.created_by
2287 }
2288
2289 pub fn created_at(&self) -> DateTime<Utc> {
2291 self.created_at
2292 }
2293
2294 pub fn image_count(&self) -> u64 {
2296 self.image_count
2297 }
2298
2299 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2301 &self.annotation_counts
2302 }
2303
2304 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2306 &self.sensor_counts
2307 }
2308
2309 pub fn label_count(&self) -> u64 {
2311 self.label_count
2312 }
2313
2314 pub fn annotation_set_count(&self) -> u64 {
2316 self.annotation_set_count
2317 }
2318
2319 pub fn snapshot_id(&self) -> Option<u64> {
2321 self.snapshot_id
2322 }
2323
2324 pub fn is_current(&self) -> bool {
2327 self.is_current
2328 }
2329}
2330
2331impl Display for VersionTag {
2332 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2333 write!(f, "{} (serial {})", self.name, self.serial)
2334 }
2335}
2336
2337#[derive(Deserialize, Serialize, Clone, Debug)]
2339pub struct ChangelogEntry {
2340 id: u64,
2341 dataset_id: DatasetID,
2342 serial: u64,
2343 entity_type: String,
2344 operation: String,
2345 #[serde(default)]
2346 entity_id: Option<u64>,
2347 #[serde(default)]
2348 change_data: serde_json::Value,
2349 username: String,
2350 organization_id: u64,
2351 created_at: DateTime<Utc>,
2352 #[serde(default)]
2353 message: String,
2354 #[serde(default, deserialize_with = "deserialize_null_as_default")]
2355 s3_version_ids: Vec<serde_json::Value>,
2356}
2357
2358impl ChangelogEntry {
2359 pub fn id(&self) -> u64 {
2360 self.id
2361 }
2362
2363 pub fn dataset_id(&self) -> DatasetID {
2364 self.dataset_id
2365 }
2366
2367 pub fn serial(&self) -> u64 {
2369 self.serial
2370 }
2371
2372 pub fn entity_type(&self) -> &str {
2374 &self.entity_type
2375 }
2376
2377 pub fn operation(&self) -> &str {
2379 &self.operation
2380 }
2381
2382 pub fn entity_id(&self) -> Option<u64> {
2383 self.entity_id
2384 }
2385
2386 pub fn change_data(&self) -> &serde_json::Value {
2388 &self.change_data
2389 }
2390
2391 pub fn username(&self) -> &str {
2392 &self.username
2393 }
2394
2395 pub fn organization_id(&self) -> u64 {
2396 self.organization_id
2397 }
2398
2399 pub fn created_at(&self) -> DateTime<Utc> {
2400 self.created_at
2401 }
2402
2403 pub fn message(&self) -> &str {
2404 &self.message
2405 }
2406
2407 pub fn s3_version_ids(&self) -> &[serde_json::Value] {
2408 &self.s3_version_ids
2409 }
2410}
2411
2412#[derive(Deserialize, Debug, Clone)]
2414pub struct ChangelogResponse {
2415 pub entries: Vec<ChangelogEntry>,
2416 pub count: u64,
2417 #[serde(default)]
2418 pub continue_token: String,
2419 #[serde(default)]
2420 pub from_serial: Option<u64>,
2421 #[serde(default)]
2422 pub to_serial: Option<u64>,
2423}
2424
2425#[derive(Deserialize, Serialize, Clone, Debug)]
2427pub struct DatasetSummary {
2428 dataset_id: DatasetID,
2429 current_serial: u64,
2430 #[serde(default)]
2431 image_count: u64,
2432 #[serde(default)]
2433 annotation_counts: HashMap<String, u64>,
2434 #[serde(default)]
2435 sensor_counts: HashMap<String, u64>,
2436 #[serde(default)]
2437 label_count: u64,
2438 #[serde(default)]
2439 annotation_set_count: u64,
2440 last_updated: DateTime<Utc>,
2441}
2442
2443impl DatasetSummary {
2444 pub fn dataset_id(&self) -> DatasetID {
2445 self.dataset_id
2446 }
2447
2448 pub fn current_serial(&self) -> u64 {
2449 self.current_serial
2450 }
2451
2452 pub fn image_count(&self) -> u64 {
2453 self.image_count
2454 }
2455
2456 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2457 &self.annotation_counts
2458 }
2459
2460 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2461 &self.sensor_counts
2462 }
2463
2464 pub fn label_count(&self) -> u64 {
2465 self.label_count
2466 }
2467
2468 pub fn annotation_set_count(&self) -> u64 {
2469 self.annotation_set_count
2470 }
2471
2472 pub fn last_updated(&self) -> DateTime<Utc> {
2473 self.last_updated
2474 }
2475}
2476
2477#[derive(Deserialize, Debug, Clone)]
2479pub struct VersionCurrentResponse {
2480 pub dataset_id: DatasetID,
2481 pub current_serial: u64,
2482 #[serde(default)]
2483 pub latest_tag: Option<VersionTag>,
2484 #[serde(default)]
2485 pub tags: Vec<VersionTag>,
2486 #[serde(default)]
2487 pub summary: Option<DatasetSummary>,
2488}
2489
2490#[derive(Deserialize, Debug, Clone)]
2492pub struct RestoredFrom {
2493 pub tag: String,
2494 pub serial: u64,
2495}
2496
2497#[derive(Deserialize, Debug, Clone)]
2499pub struct RestoredCounts {
2500 pub images: u64,
2501 pub labels: u64,
2502 pub annotation_sets: u64,
2503}
2504
2505#[derive(Deserialize, Debug, Clone)]
2507pub struct RestoreResult {
2508 pub success: bool,
2509 pub new_serial: u64,
2510 pub restored_from: RestoredFrom,
2511 pub restored_counts: RestoredCounts,
2512 pub message: String,
2513}
2514
2515#[derive(Serialize)]
2518pub(crate) struct VersionTagCreateParams {
2519 pub dataset_id: DatasetID,
2520 pub name: String,
2521 #[serde(skip_serializing_if = "Option::is_none")]
2522 pub description: Option<String>,
2523}
2524
2525#[derive(Serialize)]
2526pub(crate) struct VersionTagNameParams {
2527 pub dataset_id: DatasetID,
2528 pub name: String,
2529}
2530
2531#[derive(Serialize)]
2532pub(crate) struct VersionChangelogParams {
2533 pub dataset_id: DatasetID,
2534 #[serde(skip_serializing_if = "Option::is_none")]
2535 pub from_version: Option<String>,
2536 #[serde(skip_serializing_if = "Option::is_none")]
2537 pub to_version: Option<String>,
2538 #[serde(skip_serializing_if = "Option::is_none")]
2539 pub entity_types: Option<Vec<String>>,
2540 #[serde(skip_serializing_if = "Option::is_none")]
2541 pub limit: Option<u64>,
2542 #[serde(skip_serializing_if = "Option::is_none")]
2543 pub continue_token: Option<String>,
2544}
2545
2546#[derive(Deserialize, Debug)]
2548pub(crate) struct ChangelogCountResult {
2549 pub count: u64,
2550}
2551
2552#[derive(Serialize, Deserialize, Debug, Clone)]
2559pub struct TrainerSchemaInfo {
2560 pub name: String,
2562 #[serde(default)]
2564 pub label: String,
2565 #[serde(default)]
2567 pub schema_type: String,
2568}
2569
2570#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
2576#[serde(rename_all = "lowercase")]
2577pub enum SchemaFieldType {
2578 Group,
2580 Slider,
2582 Select,
2584 Bool,
2586 Int,
2588 Float,
2590 Text,
2592 Date,
2594 Project,
2596 Dataset,
2598 Trainer,
2600 Upload,
2602 Info,
2605 #[serde(other)]
2607 Unknown,
2608}
2609
2610fn lenient_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
2614where
2615 D: Deserializer<'de>,
2616{
2617 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
2618 Ok(value.map(|v| match v {
2619 serde_json::Value::String(s) => s,
2620 other => other.to_string(),
2621 }))
2622}
2623
2624#[derive(Serialize, Deserialize, Debug, Clone)]
2626pub struct SchemaOption {
2627 #[serde(default)]
2629 pub name: Option<Parameter>,
2630 #[serde(default, deserialize_with = "lenient_string")]
2633 pub label: Option<String>,
2634 #[serde(default)]
2636 pub children: Vec<SchemaField>,
2637}
2638
2639#[derive(Serialize, Deserialize, Debug, Clone)]
2651pub struct SchemaField {
2652 #[serde(default, deserialize_with = "lenient_string")]
2654 pub name: Option<String>,
2655 #[serde(default, deserialize_with = "lenient_string")]
2657 pub label: Option<String>,
2658 #[serde(default, deserialize_with = "lenient_string")]
2660 pub description: Option<String>,
2661 #[serde(default)]
2663 pub required: bool,
2664 #[serde(default)]
2666 pub default: Option<Parameter>,
2667 #[serde(rename = "type", default)]
2669 pub field_type: Option<SchemaFieldType>,
2670 #[serde(default)]
2672 pub min: Option<f64>,
2673 #[serde(default)]
2675 pub max: Option<f64>,
2676 #[serde(default)]
2678 pub step: Option<f64>,
2679 #[serde(default)]
2681 pub options: Vec<SchemaOption>,
2682 #[serde(default)]
2685 pub children: Vec<SchemaField>,
2686 #[serde(default)]
2688 pub is_dropdown: bool,
2689 #[serde(default)]
2691 pub multi_select: bool,
2692 #[serde(default)]
2694 pub is_multi_line: bool,
2695 #[serde(default)]
2697 pub hidden: bool,
2698 #[serde(default)]
2700 pub numeric_only: bool,
2701 #[serde(default)]
2703 pub enable_tags_selection: bool,
2704 #[serde(default)]
2706 pub enable_annotation_set_selection: bool,
2707 #[serde(default)]
2709 pub values: Option<Vec<Parameter>>,
2710}
2711
2712#[derive(Serialize, Deserialize, Debug, Clone)]
2715pub struct ValidatorSchema {
2716 #[serde(rename = "type", default)]
2718 pub schema_type: String,
2719 #[serde(default)]
2721 pub name: String,
2722 #[serde(default)]
2724 pub schema: Vec<SchemaField>,
2725}
2726
2727#[cfg(test)]
2728mod tests {
2729 use super::*;
2730
2731 #[test]
2733 fn test_organization_id_from_u64() {
2734 let id = OrganizationID::from(12345);
2735 assert_eq!(id.value(), 12345);
2736 }
2737
2738 #[test]
2739 fn test_organization_id_display() {
2740 let id = OrganizationID::from(0xabc123);
2741 assert_eq!(format!("{}", id), "org-abc123");
2742 }
2743
2744 #[test]
2745 fn test_organization_id_try_from_str_valid() {
2746 let id = OrganizationID::try_from("org-abc123").unwrap();
2747 assert_eq!(id.value(), 0xabc123);
2748 }
2749
2750 #[test]
2751 fn test_organization_id_try_from_str_invalid_prefix() {
2752 let result = OrganizationID::try_from("invalid-abc123");
2753 assert!(result.is_err());
2754 match result {
2755 Err(Error::InvalidParameters(msg)) => {
2756 assert!(msg.contains("must start with 'org-'"));
2757 }
2758 _ => panic!("Expected InvalidParameters error"),
2759 }
2760 }
2761
2762 #[test]
2763 fn test_organization_id_try_from_str_invalid_hex() {
2764 let result = OrganizationID::try_from("org-xyz");
2765 assert!(result.is_err());
2766 }
2767
2768 #[test]
2769 fn test_organization_id_try_from_str_empty() {
2770 let result = OrganizationID::try_from("org-");
2771 assert!(result.is_err());
2772 }
2773
2774 #[test]
2775 fn test_organization_id_into_u64() {
2776 let id = OrganizationID::from(54321);
2777 let value: u64 = id.into();
2778 assert_eq!(value, 54321);
2779 }
2780
2781 #[test]
2783 fn test_usage_summary_deserialize_and_accessors() {
2784 let usage: UsageSummary = serde_json::from_str(
2785 r#"{"credits": 12.5, "funds": 49092.92, "total_funds_and_credits": 49105.42}"#,
2786 )
2787 .unwrap();
2788 assert_eq!(usage.credits(), 12.5);
2789 assert_eq!(usage.funds(), 49092.92);
2790 assert_eq!(usage.total(), 49105.42);
2791 }
2792
2793 #[test]
2794 fn test_usage_summary_defaults_for_missing_fields() {
2795 let usage: UsageSummary = serde_json::from_str("{}").unwrap();
2799 assert_eq!(usage.credits(), 0.0);
2800 assert_eq!(usage.funds(), 0.0);
2801 assert_eq!(usage.total(), 0.0);
2802 }
2803
2804 #[test]
2806 fn test_project_id_from_u64() {
2807 let id = ProjectID::from(78910);
2808 assert_eq!(id.value(), 78910);
2809 }
2810
2811 #[test]
2812 fn test_project_id_display() {
2813 let id = ProjectID::from(0xdef456);
2814 assert_eq!(format!("{}", id), "p-def456");
2815 }
2816
2817 #[test]
2818 fn test_project_id_from_str_valid() {
2819 let id = ProjectID::from_str("p-def456").unwrap();
2820 assert_eq!(id.value(), 0xdef456);
2821 }
2822
2823 #[test]
2824 fn test_project_id_try_from_str_valid() {
2825 let id = ProjectID::try_from("p-123abc").unwrap();
2826 assert_eq!(id.value(), 0x123abc);
2827 }
2828
2829 #[test]
2830 fn test_project_id_try_from_string_valid() {
2831 let id = ProjectID::try_from("p-456def".to_string()).unwrap();
2832 assert_eq!(id.value(), 0x456def);
2833 }
2834
2835 #[test]
2836 fn test_project_id_from_str_invalid_prefix() {
2837 let result = ProjectID::from_str("proj-123");
2838 assert!(result.is_err());
2839 match result {
2840 Err(Error::InvalidParameters(msg)) => {
2841 assert!(msg.contains("must start with 'p-'"));
2842 }
2843 _ => panic!("Expected InvalidParameters error"),
2844 }
2845 }
2846
2847 #[test]
2848 fn test_project_id_from_str_invalid_hex() {
2849 let result = ProjectID::from_str("p-notahex");
2850 assert!(result.is_err());
2851 }
2852
2853 #[test]
2854 fn test_project_id_into_u64() {
2855 let id = ProjectID::from(99999);
2856 let value: u64 = id.into();
2857 assert_eq!(value, 99999);
2858 }
2859
2860 #[test]
2862 fn test_experiment_id_from_u64() {
2863 let id = ExperimentID::from(1193046);
2864 assert_eq!(id.value(), 1193046);
2865 }
2866
2867 #[test]
2868 fn test_experiment_id_display() {
2869 let id = ExperimentID::from(0x123abc);
2870 assert_eq!(format!("{}", id), "exp-123abc");
2871 }
2872
2873 #[test]
2874 fn test_experiment_id_from_str_valid() {
2875 let id = ExperimentID::from_str("exp-456def").unwrap();
2876 assert_eq!(id.value(), 0x456def);
2877 }
2878
2879 #[test]
2880 fn test_experiment_id_try_from_str_valid() {
2881 let id = ExperimentID::try_from("exp-789abc").unwrap();
2882 assert_eq!(id.value(), 0x789abc);
2883 }
2884
2885 #[test]
2886 fn test_experiment_id_try_from_string_valid() {
2887 let id = ExperimentID::try_from("exp-fedcba".to_string()).unwrap();
2888 assert_eq!(id.value(), 0xfedcba);
2889 }
2890
2891 #[test]
2892 fn test_experiment_id_from_str_invalid_prefix() {
2893 let result = ExperimentID::from_str("experiment-123");
2894 assert!(result.is_err());
2895 match result {
2896 Err(Error::InvalidParameters(msg)) => {
2897 assert!(msg.contains("must start with 'exp-'"));
2898 }
2899 _ => panic!("Expected InvalidParameters error"),
2900 }
2901 }
2902
2903 #[test]
2904 fn test_experiment_id_from_str_invalid_hex() {
2905 let result = ExperimentID::from_str("exp-zzz");
2906 assert!(result.is_err());
2907 }
2908
2909 #[test]
2910 fn test_experiment_id_into_u64() {
2911 let id = ExperimentID::from(777777);
2912 let value: u64 = id.into();
2913 assert_eq!(value, 777777);
2914 }
2915
2916 #[test]
2918 fn test_training_session_id_from_u64() {
2919 let id = TrainingSessionID::from(7901234);
2920 assert_eq!(id.value(), 7901234);
2921 }
2922
2923 #[test]
2924 fn test_training_session_id_display() {
2925 let id = TrainingSessionID::from(0xabc123);
2926 assert_eq!(format!("{}", id), "t-abc123");
2927 }
2928
2929 #[test]
2930 fn test_training_session_id_from_str_valid() {
2931 let id = TrainingSessionID::from_str("t-abc123").unwrap();
2932 assert_eq!(id.value(), 0xabc123);
2933 }
2934
2935 #[test]
2936 fn test_training_session_id_try_from_str_valid() {
2937 let id = TrainingSessionID::try_from("t-deadbeef").unwrap();
2938 assert_eq!(id.value(), 0xdeadbeef);
2939 }
2940
2941 #[test]
2942 fn test_training_session_id_try_from_string_valid() {
2943 let id = TrainingSessionID::try_from("t-cafebabe".to_string()).unwrap();
2944 assert_eq!(id.value(), 0xcafebabe);
2945 }
2946
2947 #[test]
2948 fn test_training_session_id_from_str_invalid_prefix() {
2949 let result = TrainingSessionID::from_str("training-123");
2950 assert!(result.is_err());
2951 match result {
2952 Err(Error::InvalidParameters(msg)) => {
2953 assert!(msg.contains("must start with 't-'"));
2954 }
2955 _ => panic!("Expected InvalidParameters error"),
2956 }
2957 }
2958
2959 #[test]
2960 fn test_training_session_id_from_str_invalid_hex() {
2961 let result = TrainingSessionID::from_str("t-qqq");
2962 assert!(result.is_err());
2963 }
2964
2965 #[test]
2966 fn test_training_session_id_into_u64() {
2967 let id = TrainingSessionID::from(123456);
2968 let value: u64 = id.into();
2969 assert_eq!(value, 123456);
2970 }
2971
2972 #[test]
2974 fn test_validation_session_id_from_u64() {
2975 let id = ValidationSessionID::from(3456789);
2976 assert_eq!(id.value(), 3456789);
2977 }
2978
2979 #[test]
2980 fn test_validation_session_id_display() {
2981 let id = ValidationSessionID::from(0x34c985);
2982 assert_eq!(format!("{}", id), "v-34c985");
2983 }
2984
2985 #[test]
2986 fn test_validation_session_id_try_from_str_valid() {
2987 let id = ValidationSessionID::try_from("v-deadbeef").unwrap();
2988 assert_eq!(id.value(), 0xdeadbeef);
2989 }
2990
2991 #[test]
2992 fn test_validation_session_id_try_from_string_valid() {
2993 let id = ValidationSessionID::try_from("v-12345678".to_string()).unwrap();
2994 assert_eq!(id.value(), 0x12345678);
2995 }
2996
2997 #[test]
2998 fn test_validation_session_id_try_from_str_invalid_prefix() {
2999 let result = ValidationSessionID::try_from("validation-123");
3000 assert!(result.is_err());
3001 match result {
3002 Err(Error::InvalidParameters(msg)) => {
3003 assert!(msg.contains("must start with 'v-'"));
3004 }
3005 _ => panic!("Expected InvalidParameters error"),
3006 }
3007 }
3008
3009 #[test]
3010 fn test_validation_session_id_try_from_str_invalid_hex() {
3011 let result = ValidationSessionID::try_from("v-xyz");
3012 assert!(result.is_err());
3013 }
3014
3015 #[test]
3016 fn test_validation_session_id_into_u64() {
3017 let id = ValidationSessionID::from(987654);
3018 let value: u64 = id.into();
3019 assert_eq!(value, 987654);
3020 }
3021
3022 #[test]
3024 fn test_snapshot_id_from_u64() {
3025 let id = SnapshotID::from(111222);
3026 assert_eq!(id.value(), 111222);
3027 }
3028
3029 #[test]
3030 fn test_snapshot_id_display() {
3031 let id = SnapshotID::from(0xaabbcc);
3032 assert_eq!(format!("{}", id), "ss-aabbcc");
3033 }
3034
3035 #[test]
3036 fn test_snapshot_id_try_from_str_valid() {
3037 let id = SnapshotID::try_from("ss-aabbcc").unwrap();
3038 assert_eq!(id.value(), 0xaabbcc);
3039 }
3040
3041 #[test]
3042 fn test_snapshot_id_try_from_str_invalid_prefix() {
3043 let result = SnapshotID::try_from("snapshot-123");
3044 assert!(result.is_err());
3045 match result {
3046 Err(Error::InvalidParameters(msg)) => {
3047 assert!(msg.contains("must start with 'ss-'"));
3048 }
3049 _ => panic!("Expected InvalidParameters error"),
3050 }
3051 }
3052
3053 #[test]
3054 fn test_snapshot_id_try_from_str_invalid_hex() {
3055 let result = SnapshotID::try_from("ss-ggg");
3056 assert!(result.is_err());
3057 }
3058
3059 #[test]
3060 fn test_snapshot_id_into_u64() {
3061 let id = SnapshotID::from(333444);
3062 let value: u64 = id.into();
3063 assert_eq!(value, 333444);
3064 }
3065
3066 #[test]
3068 fn test_task_id_from_u64() {
3069 let id = TaskID::from(555666);
3070 assert_eq!(id.value(), 555666);
3071 }
3072
3073 #[test]
3074 fn test_task_id_display() {
3075 let id = TaskID::from(0x123456);
3076 assert_eq!(format!("{}", id), "task-123456");
3077 }
3078
3079 #[test]
3080 fn test_task_id_from_str_valid() {
3081 let id = TaskID::from_str("task-123456").unwrap();
3082 assert_eq!(id.value(), 0x123456);
3083 }
3084
3085 #[test]
3086 fn test_task_id_try_from_str_valid() {
3087 let id = TaskID::try_from("task-abcdef").unwrap();
3088 assert_eq!(id.value(), 0xabcdef);
3089 }
3090
3091 #[test]
3092 fn test_task_id_try_from_string_valid() {
3093 let id = TaskID::try_from("task-fedcba".to_string()).unwrap();
3094 assert_eq!(id.value(), 0xfedcba);
3095 }
3096
3097 #[test]
3098 fn test_task_id_from_str_invalid_prefix() {
3099 let result = TaskID::from_str("t-123");
3100 assert!(result.is_err());
3101 match result {
3102 Err(Error::InvalidParameters(msg)) => {
3103 assert!(msg.contains("must start with 'task-'"));
3104 }
3105 _ => panic!("Expected InvalidParameters error"),
3106 }
3107 }
3108
3109 #[test]
3110 fn test_task_id_from_str_invalid_hex() {
3111 let result = TaskID::from_str("task-zzz");
3112 assert!(result.is_err());
3113 }
3114
3115 #[test]
3116 fn test_task_id_into_u64() {
3117 let id = TaskID::from(777888);
3118 let value: u64 = id.into();
3119 assert_eq!(value, 777888);
3120 }
3121
3122 #[test]
3124 fn test_dataset_id_from_u64() {
3125 let id = DatasetID::from(1193046);
3126 assert_eq!(id.value(), 1193046);
3127 }
3128
3129 #[test]
3130 fn test_dataset_id_display() {
3131 let id = DatasetID::from(0x123abc);
3132 assert_eq!(format!("{}", id), "ds-123abc");
3133 }
3134
3135 #[test]
3136 fn test_dataset_id_from_str_valid() {
3137 let id = DatasetID::from_str("ds-456def").unwrap();
3138 assert_eq!(id.value(), 0x456def);
3139 }
3140
3141 #[test]
3142 fn test_dataset_id_try_from_str_valid() {
3143 let id = DatasetID::try_from("ds-789abc").unwrap();
3144 assert_eq!(id.value(), 0x789abc);
3145 }
3146
3147 #[test]
3148 fn test_dataset_id_try_from_string_valid() {
3149 let id = DatasetID::try_from("ds-fedcba".to_string()).unwrap();
3150 assert_eq!(id.value(), 0xfedcba);
3151 }
3152
3153 #[test]
3154 fn test_dataset_id_from_str_invalid_prefix() {
3155 let result = DatasetID::from_str("dataset-123");
3156 assert!(result.is_err());
3157 match result {
3158 Err(Error::InvalidParameters(msg)) => {
3159 assert!(msg.contains("must start with 'ds-'"));
3160 }
3161 _ => panic!("Expected InvalidParameters error"),
3162 }
3163 }
3164
3165 #[test]
3166 fn test_dataset_id_from_str_invalid_hex() {
3167 let result = DatasetID::from_str("ds-zzz");
3168 assert!(result.is_err());
3169 }
3170
3171 #[test]
3172 fn test_dataset_id_into_u64() {
3173 let id = DatasetID::from(111111);
3174 let value: u64 = id.into();
3175 assert_eq!(value, 111111);
3176 }
3177
3178 #[test]
3180 fn test_annotation_set_id_from_u64() {
3181 let id = AnnotationSetID::from(222333);
3182 assert_eq!(id.value(), 222333);
3183 }
3184
3185 #[test]
3186 fn test_annotation_set_id_display() {
3187 let id = AnnotationSetID::from(0xabcdef);
3188 assert_eq!(format!("{}", id), "as-abcdef");
3189 }
3190
3191 #[test]
3192 fn test_annotation_set_id_from_str_valid() {
3193 let id = AnnotationSetID::from_str("as-abcdef").unwrap();
3194 assert_eq!(id.value(), 0xabcdef);
3195 }
3196
3197 #[test]
3198 fn test_annotation_set_id_try_from_str_valid() {
3199 let id = AnnotationSetID::try_from("as-123456").unwrap();
3200 assert_eq!(id.value(), 0x123456);
3201 }
3202
3203 #[test]
3204 fn test_annotation_set_id_try_from_string_valid() {
3205 let id = AnnotationSetID::try_from("as-fedcba".to_string()).unwrap();
3206 assert_eq!(id.value(), 0xfedcba);
3207 }
3208
3209 #[test]
3210 fn test_annotation_set_id_from_str_invalid_prefix() {
3211 let result = AnnotationSetID::from_str("annotation-123");
3212 assert!(result.is_err());
3213 match result {
3214 Err(Error::InvalidParameters(msg)) => {
3215 assert!(msg.contains("must start with 'as-'"));
3216 }
3217 _ => panic!("Expected InvalidParameters error"),
3218 }
3219 }
3220
3221 #[test]
3222 fn test_annotation_set_id_from_str_invalid_hex() {
3223 let result = AnnotationSetID::from_str("as-zzz");
3224 assert!(result.is_err());
3225 }
3226
3227 #[test]
3228 fn test_annotation_set_id_into_u64() {
3229 let id = AnnotationSetID::from(444555);
3230 let value: u64 = id.into();
3231 assert_eq!(value, 444555);
3232 }
3233
3234 #[test]
3236 fn test_sample_id_from_u64() {
3237 let id = SampleID::from(666777);
3238 assert_eq!(id.value(), 666777);
3239 }
3240
3241 #[test]
3242 fn test_sample_id_display() {
3243 let id = SampleID::from(0x987654);
3244 assert_eq!(format!("{}", id), "s-987654");
3245 }
3246
3247 #[test]
3248 fn test_sample_id_try_from_str_valid() {
3249 let id = SampleID::try_from("s-987654").unwrap();
3250 assert_eq!(id.value(), 0x987654);
3251 }
3252
3253 #[test]
3254 fn test_sample_id_try_from_str_invalid_prefix() {
3255 let result = SampleID::try_from("sample-123");
3256 assert!(result.is_err());
3257 match result {
3258 Err(Error::InvalidParameters(msg)) => {
3259 assert!(msg.contains("must start with 's-'"));
3260 }
3261 _ => panic!("Expected InvalidParameters error"),
3262 }
3263 }
3264
3265 #[test]
3266 fn test_sample_id_try_from_str_invalid_hex() {
3267 let result = SampleID::try_from("s-zzz");
3268 assert!(result.is_err());
3269 }
3270
3271 #[test]
3272 fn test_sample_id_into_u64() {
3273 let id = SampleID::from(888999);
3274 let value: u64 = id.into();
3275 assert_eq!(value, 888999);
3276 }
3277
3278 #[test]
3280 fn test_app_id_from_u64() {
3281 let id = AppId::from(123123);
3282 assert_eq!(id.value(), 123123);
3283 }
3284
3285 #[test]
3286 fn test_app_id_display() {
3287 let id = AppId::from(0x456789);
3288 assert_eq!(format!("{}", id), "app-456789");
3289 }
3290
3291 #[test]
3292 fn test_app_id_try_from_str_valid() {
3293 let id = AppId::try_from("app-456789").unwrap();
3294 assert_eq!(id.value(), 0x456789);
3295 }
3296
3297 #[test]
3298 fn test_app_id_try_from_str_invalid_prefix() {
3299 let result = AppId::try_from("application-123");
3300 assert!(result.is_err());
3301 match result {
3302 Err(Error::InvalidParameters(msg)) => {
3303 assert!(msg.contains("must start with 'app-'"));
3304 }
3305 _ => panic!("Expected InvalidParameters error"),
3306 }
3307 }
3308
3309 #[test]
3310 fn test_app_id_try_from_str_invalid_hex() {
3311 let result = AppId::try_from("app-zzz");
3312 assert!(result.is_err());
3313 }
3314
3315 #[test]
3316 fn test_app_id_into_u64() {
3317 let id = AppId::from(321321);
3318 let value: u64 = id.into();
3319 assert_eq!(value, 321321);
3320 }
3321
3322 #[test]
3324 fn test_image_id_from_u64() {
3325 let id = ImageId::from(789789);
3326 assert_eq!(id.value(), 789789);
3327 }
3328
3329 #[test]
3330 fn test_image_id_display() {
3331 let id = ImageId::from(0xabcd1234);
3332 assert_eq!(format!("{}", id), "im-abcd1234");
3333 }
3334
3335 #[test]
3336 fn test_image_id_try_from_str_valid() {
3337 let id = ImageId::try_from("im-abcd1234").unwrap();
3338 assert_eq!(id.value(), 0xabcd1234);
3339 }
3340
3341 #[test]
3342 fn test_image_id_try_from_str_invalid_prefix() {
3343 let result = ImageId::try_from("image-123");
3344 assert!(result.is_err());
3345 match result {
3346 Err(Error::InvalidParameters(msg)) => {
3347 assert!(msg.contains("must start with 'im-'"));
3348 }
3349 _ => panic!("Expected InvalidParameters error"),
3350 }
3351 }
3352
3353 #[test]
3354 fn test_image_id_try_from_str_invalid_hex() {
3355 let result = ImageId::try_from("im-zzz");
3356 assert!(result.is_err());
3357 }
3358
3359 #[test]
3360 fn test_image_id_into_u64() {
3361 let id = ImageId::from(987987);
3362 let value: u64 = id.into();
3363 assert_eq!(value, 987987);
3364 }
3365
3366 #[test]
3368 fn test_id_types_equality() {
3369 let id1 = ProjectID::from(12345);
3370 let id2 = ProjectID::from(12345);
3371 let id3 = ProjectID::from(54321);
3372
3373 assert_eq!(id1, id2);
3374 assert_ne!(id1, id3);
3375 }
3376
3377 #[test]
3378 fn test_id_types_hash() {
3379 use std::collections::HashSet;
3380
3381 let mut set = HashSet::new();
3382 set.insert(DatasetID::from(100));
3383 set.insert(DatasetID::from(200));
3384 set.insert(DatasetID::from(100)); assert_eq!(set.len(), 2);
3387 assert!(set.contains(&DatasetID::from(100)));
3388 assert!(set.contains(&DatasetID::from(200)));
3389 }
3390
3391 #[test]
3392 fn test_id_types_copy_clone() {
3393 let id1 = ExperimentID::from(999);
3394 let id2 = id1; let id3 = id1; assert_eq!(id1, id2);
3398 assert_eq!(id1, id3);
3399 }
3400
3401 #[test]
3403 fn test_id_zero_value() {
3404 let id = ProjectID::from(0);
3405 assert_eq!(format!("{}", id), "p-0");
3406 assert_eq!(id.value(), 0);
3407 }
3408
3409 #[test]
3410 fn test_id_max_value() {
3411 let id = ProjectID::from(u64::MAX);
3412 assert_eq!(format!("{}", id), "p-ffffffffffffffff");
3413 assert_eq!(id.value(), u64::MAX);
3414 }
3415
3416 #[test]
3417 fn test_id_round_trip_conversion() {
3418 let original = 0xdeadbeef_u64;
3419 let id = TrainingSessionID::from(original);
3420 let back: u64 = id.into();
3421 assert_eq!(original, back);
3422 }
3423
3424 #[test]
3425 fn test_id_case_insensitive_hex() {
3426 let id1 = DatasetID::from_str("ds-ABCDEF").unwrap();
3428 let id2 = DatasetID::from_str("ds-abcdef").unwrap();
3429 assert_eq!(id1.value(), id2.value());
3430 }
3431
3432 #[test]
3433 fn test_id_with_leading_zeros() {
3434 let id = ProjectID::from_str("p-00001234").unwrap();
3435 assert_eq!(id.value(), 0x1234);
3436 }
3437
3438 #[test]
3440 fn test_parameter_integer() {
3441 let param = Parameter::Integer(42);
3442 match param {
3443 Parameter::Integer(val) => assert_eq!(val, 42),
3444 _ => panic!("Expected Integer variant"),
3445 }
3446 }
3447
3448 #[test]
3449 fn test_parameter_real() {
3450 let param = Parameter::Real(2.5);
3451 match param {
3452 Parameter::Real(val) => assert_eq!(val, 2.5),
3453 _ => panic!("Expected Real variant"),
3454 }
3455 }
3456
3457 #[test]
3458 fn test_parameter_boolean() {
3459 let param = Parameter::Boolean(true);
3460 match param {
3461 Parameter::Boolean(val) => assert!(val),
3462 _ => panic!("Expected Boolean variant"),
3463 }
3464 }
3465
3466 #[test]
3467 fn test_parameter_string() {
3468 let param = Parameter::String("test".to_string());
3469 match param {
3470 Parameter::String(val) => assert_eq!(val, "test"),
3471 _ => panic!("Expected String variant"),
3472 }
3473 }
3474
3475 #[test]
3476 fn test_parameter_array() {
3477 let param = Parameter::Array(vec![
3478 Parameter::Integer(1),
3479 Parameter::Integer(2),
3480 Parameter::Integer(3),
3481 ]);
3482 match param {
3483 Parameter::Array(arr) => assert_eq!(arr.len(), 3),
3484 _ => panic!("Expected Array variant"),
3485 }
3486 }
3487
3488 #[test]
3489 fn test_parameter_object() {
3490 let mut map = HashMap::new();
3491 map.insert("key".to_string(), Parameter::Integer(100));
3492 let param = Parameter::Object(map);
3493 match param {
3494 Parameter::Object(obj) => {
3495 assert_eq!(obj.len(), 1);
3496 assert!(obj.contains_key("key"));
3497 }
3498 _ => panic!("Expected Object variant"),
3499 }
3500 }
3501
3502 #[test]
3503 fn test_parameter_clone() {
3504 let param1 = Parameter::Integer(42);
3505 let param2 = param1.clone();
3506 assert_eq!(param1, param2);
3507 }
3508
3509 #[test]
3510 fn test_parameter_nested() {
3511 let inner_array = Parameter::Array(vec![Parameter::Integer(1), Parameter::Integer(2)]);
3512 let outer_array = Parameter::Array(vec![inner_array.clone(), inner_array]);
3513
3514 match outer_array {
3515 Parameter::Array(arr) => {
3516 assert_eq!(arr.len(), 2);
3517 }
3518 _ => panic!("Expected Array variant"),
3519 }
3520 }
3521
3522 macro_rules! test_typeid_conversions {
3525 ($test_name:ident, $type:ty, $prefix:literal, $wrong_prefix:literal) => {
3526 #[test]
3527 fn $test_name() {
3528 let id = <$type>::from(0xabc123);
3530 assert_eq!(id.value(), 0xabc123);
3531
3532 assert_eq!(format!("{}", id), concat!($prefix, "-abc123"));
3534
3535 let id: $type = concat!($prefix, "-abc123").parse().unwrap();
3537 assert_eq!(id.value(), 0xabc123);
3538
3539 assert!(concat!($wrong_prefix, "-abc").parse::<$type>().is_err());
3541
3542 assert!("abc123".parse::<$type>().is_err());
3544
3545 assert!(concat!($prefix, "-xyz").parse::<$type>().is_err());
3547
3548 let id = <$type>::try_from(concat!($prefix, "-abc123")).unwrap();
3550 assert_eq!(id.value(), 0xabc123);
3551
3552 let id = <$type>::try_from(concat!($prefix, "-abc123").to_string()).unwrap();
3554 assert_eq!(id.value(), 0xabc123);
3555
3556 let id = <$type>::from(0xabc123);
3558 let json = serde_json::to_string(&id).unwrap();
3559 let parsed: $type = serde_json::from_str(&json).unwrap();
3560 assert_eq!(id, parsed);
3561
3562 let id = <$type>::from(0xabc123);
3564 let val: u64 = id.into();
3565 assert_eq!(val, 0xabc123);
3566 }
3567 };
3568 }
3569
3570 test_typeid_conversions!(test_organization_id_conversions, OrganizationID, "org", "p");
3571 test_typeid_conversions!(test_project_id_conversions, ProjectID, "p", "org");
3572 test_typeid_conversions!(test_experiment_id_conversions, ExperimentID, "exp", "p");
3573 test_typeid_conversions!(
3574 test_training_session_id_conversions,
3575 TrainingSessionID,
3576 "t",
3577 "v"
3578 );
3579 test_typeid_conversions!(
3580 test_validation_session_id_conversions,
3581 ValidationSessionID,
3582 "v",
3583 "t"
3584 );
3585 test_typeid_conversions!(test_snapshot_id_conversions, SnapshotID, "ss", "ds");
3586 test_typeid_conversions!(test_task_id_conversions, TaskID, "task", "t");
3587 test_typeid_conversions!(test_dataset_id_conversions, DatasetID, "ds", "ss");
3588 test_typeid_conversions!(
3589 test_annotation_set_id_conversions,
3590 AnnotationSetID,
3591 "as",
3592 "ds"
3593 );
3594 test_typeid_conversions!(test_sample_id_conversions, SampleID, "s", "p");
3595 test_typeid_conversions!(test_app_id_conversions, AppId, "app", "p");
3596 test_typeid_conversions!(test_image_id_conversions, ImageId, "im", "se");
3597 test_typeid_conversions!(test_sequence_id_conversions, SequenceId, "se", "im");
3598
3599 #[test]
3602 fn test_version_tag_deserialize_full() {
3603 let json = r#"{
3604 "id": 456, "dataset_id": 1715004, "name": "training-v1.0",
3605 "serial": 42, "description": "Ready for production",
3606 "created_by": "user@example.com", "created_at": "2025-01-15T10:30:00Z",
3607 "image_count": 50000, "annotation_counts": {"box": 150000, "seg": 20000},
3608 "sensor_counts": {"lidar": 25000}, "label_count": 15,
3609 "annotation_set_count": 3, "snapshot_id": 789
3610 }"#;
3611 let tag: VersionTag = serde_json::from_str(json).unwrap();
3612 assert_eq!(tag.name(), "training-v1.0");
3613 assert_eq!(tag.serial(), 42);
3614 assert_eq!(tag.image_count(), 50000);
3615 assert_eq!(tag.annotation_counts().get("box"), Some(&150000));
3616 assert_eq!(tag.snapshot_id(), Some(789));
3617 }
3618
3619 #[test]
3620 fn test_version_tag_deserialize_omitempty() {
3621 let json = r#"{
3623 "id": 1, "dataset_id": 2, "name": "v1.0", "serial": 5,
3624 "description": "", "created_by": "user",
3625 "created_at": "2025-01-01T00:00:00Z"
3626 }"#;
3627 let tag: VersionTag = serde_json::from_str(json).unwrap();
3628 assert_eq!(tag.snapshot_id(), None);
3629 assert_eq!(tag.image_count(), 0);
3630 assert!(tag.annotation_counts().is_empty());
3631 }
3632
3633 #[test]
3634 fn test_changelog_entry_deserialize_omitempty() {
3635 let json = r#"{
3637 "id": 1, "dataset_id": 2, "serial": 3, "entity_type": "image",
3638 "operation": "bulk_create", "change_data": {"count": 5},
3639 "username": "user", "organization_id": 1,
3640 "created_at": "2025-01-01T00:00:00Z", "message": ""
3641 }"#;
3642 let entry: ChangelogEntry = serde_json::from_str(json).unwrap();
3643 assert!(entry.entity_id().is_none());
3644 assert!(entry.s3_version_ids().is_empty());
3645 assert_eq!(entry.entity_type(), "image");
3646 assert_eq!(entry.operation(), "bulk_create");
3647 }
3648
3649 #[test]
3650 fn test_changelog_response_deserialize() {
3651 let json = r#"{
3652 "entries": [], "count": 0, "continue_token": ""
3653 }"#;
3654 let resp: ChangelogResponse = serde_json::from_str(json).unwrap();
3655 assert!(resp.entries.is_empty());
3656 assert_eq!(resp.count, 0);
3657 assert!(resp.continue_token.is_empty());
3658 assert!(resp.from_serial.is_none());
3659 }
3660
3661 #[test]
3662 fn test_version_current_no_latest_tag() {
3663 let json = r#"{
3665 "dataset_id": 100, "current_serial": 5, "tags": []
3666 }"#;
3667 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3668 assert!(resp.latest_tag.is_none());
3669 assert!(resp.tags.is_empty());
3670 assert_eq!(resp.current_serial, 5);
3671 }
3672
3673 #[test]
3674 fn test_version_current_with_latest_tag() {
3675 let json = r#"{
3676 "dataset_id": 100, "current_serial": 42,
3677 "latest_tag": {
3678 "id": 1, "dataset_id": 100, "name": "v1.0", "serial": 42,
3679 "description": "test", "created_by": "user",
3680 "created_at": "2025-01-01T00:00:00Z",
3681 "image_count": 10, "label_count": 2, "annotation_set_count": 1
3682 },
3683 "tags": []
3684 }"#;
3685 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3686 assert!(resp.latest_tag.is_some());
3687 assert_eq!(resp.latest_tag.unwrap().name(), "v1.0");
3688 }
3689
3690 #[test]
3691 fn test_version_tag_is_current_field() {
3692 let json = r#"{
3693 "id": 1, "dataset_id": 5, "name": "v1.0", "serial": 10,
3694 "created_by": "alice", "created_at": "2026-01-01T00:00:00Z",
3695 "is_current": true
3696 }"#;
3697 let tag: VersionTag = serde_json::from_str(json).unwrap();
3698 assert!(tag.is_current());
3699 }
3700
3701 #[test]
3702 fn test_version_tag_is_current_false() {
3703 let json = r#"{
3704 "id": 1, "dataset_id": 5, "name": "v1.0", "serial": 10,
3705 "created_by": "alice", "created_at": "2026-01-01T00:00:00Z",
3706 "is_current": false
3707 }"#;
3708 let tag: VersionTag = serde_json::from_str(json).unwrap();
3709 assert!(!tag.is_current());
3710 }
3711
3712 #[test]
3713 fn test_dataset_summary_deserialize() {
3714 let json = r#"{
3715 "dataset_id": 100, "current_serial": 10,
3716 "image_count": 5000, "annotation_counts": {"box": 10000},
3717 "sensor_counts": {}, "label_count": 8,
3718 "annotation_set_count": 2, "last_updated": "2025-06-01T12:00:00Z"
3719 }"#;
3720 let summary: DatasetSummary = serde_json::from_str(json).unwrap();
3721 assert_eq!(summary.image_count(), 5000);
3722 assert_eq!(summary.label_count(), 8);
3723 assert_eq!(summary.annotation_counts().get("box"), Some(&10000));
3724 }
3725
3726 #[test]
3727 fn test_restore_result_deserialize() {
3728 let json = r#"{
3729 "success": true, "new_serial": 45,
3730 "restored_from": {"tag": "v1.0", "serial": 42},
3731 "restored_counts": {"images": 5000, "labels": 15, "annotation_sets": 3},
3732 "message": "Dataset restored to tag v1.0"
3733 }"#;
3734 let result: RestoreResult = serde_json::from_str(json).unwrap();
3735 assert!(result.success);
3736 assert_eq!(result.new_serial, 45);
3737 assert_eq!(result.restored_from.tag, "v1.0");
3738 assert_eq!(result.restored_from.serial, 42);
3739 assert_eq!(result.restored_counts.images, 5000);
3740 }
3741}
3742
3743#[cfg(test)]
3744mod tests_task_data_list {
3745 use super::*;
3746
3747 #[test]
3748 fn task_data_list_deserializes_from_server_shape() {
3749 let json = r#"{
3750 "server": "test.edgefirst.studio",
3751 "organization_uid": "org-abc123",
3752 "traces": ["trace/imx95.json"],
3753 "data": {
3754 "predictions": ["predictions.parquet"],
3755 "trace": ["imx95.json"]
3756 }
3757 }"#;
3758 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
3759 assert_eq!(parsed.server, "test.edgefirst.studio");
3760 assert_eq!(parsed.organization_uid, "org-abc123");
3761 assert_eq!(parsed.traces, vec!["trace/imx95.json"]);
3762 assert_eq!(
3763 parsed.data.get("predictions").unwrap(),
3764 &vec!["predictions.parquet".to_string()]
3765 );
3766 }
3767}
3768
3769#[cfg(test)]
3770mod tests_upload_data {
3771 #[test]
3775 fn folder_empty_string_is_normalised() {
3776 let folder: Option<&str> = Some("");
3777 assert!(folder.filter(|s| !s.is_empty()).is_none());
3778
3779 let folder_real: Option<&str> = Some("predictions");
3780 assert!(folder_real.filter(|s| !s.is_empty()).is_some());
3781 }
3782}
3783
3784#[cfg(test)]
3785mod tests_job_struct {
3786 use super::*;
3787
3788 #[test]
3789 fn job_deserializes_with_all_fields() {
3790 let json = r#"{
3791 "code": "edgefirst-validator:2.9.5",
3792 "title": "EdgeFirst Validator",
3793 "job_name": "smoke-test",
3794 "job_id": "aws-batch-abc",
3795 "state": "RUNNING",
3796 "launch": "2026-05-14T15:00:00Z",
3797 "task_id": 6789
3798 }"#;
3799 let job: Job = serde_json::from_str(json).unwrap();
3800 assert_eq!(job.code, "edgefirst-validator:2.9.5");
3801 assert_eq!(job.title, "EdgeFirst Validator");
3802 assert_eq!(job.job_name, "smoke-test");
3803 assert_eq!(job.job_id, "aws-batch-abc");
3804 assert_eq!(job.state, "RUNNING");
3805 assert!(job.launch.is_some());
3806 assert_eq!(job.task_id, 6789);
3807 }
3808
3809 #[test]
3810 fn job_tolerates_missing_optional_fields() {
3811 let json = r#"{ "task_id": 42 }"#;
3815 let job: Job = serde_json::from_str(json).unwrap();
3816 assert_eq!(job.task_id, 42);
3817 assert!(job.code.is_empty());
3818 assert!(job.title.is_empty());
3819 assert!(job.job_name.is_empty());
3820 assert!(job.job_id.is_empty());
3821 assert!(job.state.is_empty());
3822 assert!(job.launch.is_none());
3823 }
3824
3825 #[test]
3826 fn job_task_id_accessor_saturates_negative_to_zero() {
3827 let job = Job {
3832 code: String::new(),
3833 title: String::new(),
3834 job_name: String::new(),
3835 job_id: String::new(),
3836 state: String::new(),
3837 launch: None,
3838 task_id: -1,
3839 };
3840 assert_eq!(job.task_id().value(), 0);
3841 }
3842
3843 #[test]
3844 fn job_task_id_accessor_passes_through_positive_values() {
3845 let job = Job {
3846 code: String::new(),
3847 title: String::new(),
3848 job_name: String::new(),
3849 job_id: String::new(),
3850 state: String::new(),
3851 launch: None,
3852 task_id: 12345,
3853 };
3854 assert_eq!(job.task_id().value(), 12345);
3855 }
3856
3857 #[test]
3858 fn job_ignores_unknown_fields() {
3859 let json = r#"{
3863 "code": "x",
3864 "task_id": 1,
3865 "docker_task": { "image": "x" },
3866 "aws_region": "us-east-1",
3867 "tags": ["a", "b"]
3868 }"#;
3869 let job: Job = serde_json::from_str(json).unwrap();
3870 assert_eq!(job.task_id, 1);
3871 }
3872}
3873
3874#[cfg(test)]
3875mod tests_task_info_schema_tolerance {
3876 use super::*;
3877
3878 #[test]
3883 fn task_info_accepts_task_description_field() {
3884 let json = r#"{
3886 "id": 6699,
3887 "type": "edgefirst-validator:2.9.5",
3888 "task_description": "Profiler run for IMX95",
3889 "status": "running"
3890 }"#;
3891 let info: TaskInfo = serde_json::from_str(json).unwrap();
3892 assert_eq!(info.description(), "Profiler run for IMX95");
3893 }
3894
3895 #[test]
3896 fn task_info_accepts_legacy_description_field() {
3897 let json = r#"{
3899 "id": 6699,
3900 "type": "edgefirst-validator:2.9.5",
3901 "description": "Legacy description"
3902 }"#;
3903 let info: TaskInfo = serde_json::from_str(json).unwrap();
3904 assert_eq!(info.description(), "Legacy description");
3905 }
3906
3907 #[test]
3908 fn task_info_tolerates_missing_description() {
3909 let json = r#"{
3911 "id": 6699,
3912 "type": "x"
3913 }"#;
3914 let info: TaskInfo = serde_json::from_str(json).unwrap();
3915 assert!(info.description().is_empty());
3916 }
3917
3918 #[test]
3919 fn task_info_tolerates_missing_dates_via_default() {
3920 let json = r#"{
3922 "id": 6699,
3923 "type": "x"
3924 }"#;
3925 let info: TaskInfo = serde_json::from_str(json).unwrap();
3926 assert_eq!(info.id().value(), 6699);
3928 }
3929
3930 #[test]
3931 fn task_info_status_accessor_returns_option() {
3932 let json = r#"{
3933 "id": 1,
3934 "type": "x"
3935 }"#;
3936 let info: TaskInfo = serde_json::from_str(json).unwrap();
3937 assert!(info.status().is_none());
3938 }
3939
3940 #[test]
3941 fn task_info_stages_returns_empty_map_when_unset() {
3942 let json = r#"{
3943 "id": 1,
3944 "type": "x"
3945 }"#;
3946 let info: TaskInfo = serde_json::from_str(json).unwrap();
3947 let stages = info.stages();
3948 assert!(stages.is_empty());
3949 }
3950}
3951
3952#[cfg(test)]
3953mod tests_stage_struct {
3954 use super::*;
3955
3956 #[test]
3957 fn stage_new_sets_only_supplied_fields() {
3958 let stage = Stage::new(
3959 None,
3960 "download".into(),
3961 Some("running".into()),
3962 Some("fetching".into()),
3963 42,
3964 );
3965 assert!(stage.task_id().is_none());
3966 assert_eq!(stage.stage(), "download");
3967 assert_eq!(stage.status().as_deref(), Some("running"));
3968 assert_eq!(stage.message().as_deref(), Some("fetching"));
3969 assert_eq!(stage.percentage(), 42);
3970 assert!(stage.description().is_none());
3972 }
3973
3974 #[test]
3975 fn stage_serializes_without_optional_none_fields() {
3976 let stage = Stage::new(None, "init".into(), None, None, 0);
3978 let json = serde_json::to_value(&stage).unwrap();
3979 assert!(json.get("status").is_none(), "got: {json}");
3980 assert!(json.get("message").is_none(), "got: {json}");
3981 assert!(json.get("docker_task_id").is_none(), "got: {json}");
3982 assert_eq!(json["stage"], "init");
3984 assert_eq!(json["percentage"], 0);
3985 }
3986
3987 #[test]
3988 fn stage_serializes_task_id_when_present() {
3989 let task_id = TaskID::from(0xdeadu64);
3990 let stage = Stage::new(Some(task_id), "x".into(), None, None, 0);
3991 let json = serde_json::to_value(&stage).unwrap();
3992 assert!(json.get("docker_task_id").is_some());
3995 }
3996
3997 #[test]
3998 fn stage_round_trips_through_json() {
3999 let stage = Stage::new(
4000 None,
4001 "train".into(),
4002 Some("done".into()),
4003 Some("epoch 100".into()),
4004 100,
4005 );
4006 let s = serde_json::to_string(&stage).unwrap();
4007 let back: Stage = serde_json::from_str(&s).unwrap();
4008 assert_eq!(back.stage(), "train");
4009 assert_eq!(back.status().as_deref(), Some("done"));
4010 assert_eq!(back.message().as_deref(), Some("epoch 100"));
4011 assert_eq!(back.percentage(), 100);
4012 }
4013}
4014
4015#[cfg(test)]
4016mod tests_task_data_list_extra {
4017 use super::*;
4018
4019 #[test]
4020 fn task_data_list_with_empty_data_map() {
4021 let json = r#"{
4022 "server": "studio",
4023 "organization_uid": "org-1",
4024 "traces": [],
4025 "data": {}
4026 }"#;
4027 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4028 assert!(parsed.traces.is_empty());
4029 assert!(parsed.data.is_empty());
4030 }
4031
4032 #[test]
4033 fn task_data_list_multiple_folders() {
4034 let json = r#"{
4035 "server": "studio",
4036 "organization_uid": "org-1",
4037 "traces": ["t1", "t2"],
4038 "data": {
4039 "predictions": ["a.parquet", "b.parquet"],
4040 "metrics": ["loss.json"]
4041 }
4042 }"#;
4043 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4044 assert_eq!(parsed.traces.len(), 2);
4045 assert_eq!(parsed.data.len(), 2);
4046 assert_eq!(parsed.data["predictions"].len(), 2);
4047 }
4048}
4049
4050#[cfg(test)]
4051mod tests_artifact_struct {
4052 use super::*;
4053
4054 #[test]
4055 fn artifact_accessors_return_strs() {
4056 let json = r#"{ "name": "best.onnx", "modelType": "yolo" }"#;
4059 let a: Artifact = serde_json::from_str(json).unwrap();
4060 assert_eq!(a.name(), "best.onnx");
4061 assert_eq!(a.model_type(), "yolo");
4062 }
4063}
4064
4065#[cfg(test)]
4066mod tests_task_status_serialize {
4067 use super::*;
4068
4069 #[test]
4070 fn task_status_uses_docker_task_id_wire_field() {
4071 let s = TaskStatus {
4072 task_id: TaskID::from(0x1a2bu64),
4073 status: "training".into(),
4074 };
4075 let json = serde_json::to_value(&s).unwrap();
4076 assert!(json.get("docker_task_id").is_some(), "got: {json}");
4078 assert_eq!(json["status"], "training");
4079 }
4080}
4081
4082#[cfg(test)]
4083mod tests_task_stages_serialize {
4084 use super::*;
4085
4086 #[test]
4087 fn task_stages_omits_empty_vec() {
4088 let stages = TaskStages {
4089 task_id: TaskID::from(1u64),
4090 stages: Vec::new(),
4091 };
4092 let json = serde_json::to_value(&stages).unwrap();
4093 assert!(json.get("stages").is_none(), "got: {json}");
4095 }
4096
4097 #[test]
4098 fn task_stages_serializes_non_empty_vec() {
4099 let stages = TaskStages {
4100 task_id: TaskID::from(1u64),
4101 stages: vec![std::collections::HashMap::from([(
4102 "stage".to_string(),
4103 "download".to_string(),
4104 )])],
4105 };
4106 let json = serde_json::to_value(&stages).unwrap();
4107 assert_eq!(json["stages"][0]["stage"], "download");
4108 }
4109}