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(Serialize, Debug)]
690pub struct SampleDeleteParams {
691 pub dataset_id: u64,
692 pub image_ids: Vec<u64>,
693 pub sequence_ids: Vec<i64>,
694 pub delete_all: bool,
695}
696
697#[derive(Deserialize)]
698pub struct Snapshot {
699 id: SnapshotID,
700 description: String,
701 status: String,
702 path: String,
703 #[serde(rename = "date")]
704 created: DateTime<Utc>,
705}
706
707impl Display for Snapshot {
708 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
709 write!(f, "{} {}", self.id, self.description)
710 }
711}
712
713impl Snapshot {
714 pub fn id(&self) -> SnapshotID {
715 self.id
716 }
717
718 pub fn description(&self) -> &str {
719 &self.description
720 }
721
722 pub fn status(&self) -> &str {
723 &self.status
724 }
725
726 pub fn path(&self) -> &str {
727 &self.path
728 }
729
730 pub fn created(&self) -> &DateTime<Utc> {
731 &self.created
732 }
733}
734
735#[derive(Serialize, Debug)]
736pub struct SnapshotRestore {
737 pub project_id: ProjectID,
738 pub snapshot_id: SnapshotID,
739 pub fps: u64,
740 #[serde(rename = "enabled_topics", skip_serializing_if = "Vec::is_empty")]
741 pub topics: Vec<String>,
742 #[serde(rename = "label_names", skip_serializing_if = "Vec::is_empty")]
743 pub autolabel: Vec<String>,
744 #[serde(rename = "depth_gen")]
745 pub autodepth: bool,
746 pub agtg_pipeline: bool,
747 #[serde(skip_serializing_if = "Option::is_none")]
748 pub dataset_name: Option<String>,
749 #[serde(skip_serializing_if = "Option::is_none")]
750 pub dataset_description: Option<String>,
751}
752
753#[derive(Deserialize, Debug)]
754pub struct SnapshotRestoreResult {
755 pub id: SnapshotID,
756 pub description: String,
757 pub dataset_name: String,
758 pub dataset_id: DatasetID,
759 pub annotation_set_id: AnnotationSetID,
760 #[serde(default)]
761 pub task_id: Option<TaskID>,
762 #[serde(default)]
766 pub date: Option<DateTime<Utc>>,
767}
768
769#[derive(Serialize, Debug)]
774pub struct SnapshotCreateFromDataset {
775 pub description: String,
777 pub dataset_id: DatasetID,
779 pub annotation_set_id: AnnotationSetID,
781}
782
783#[derive(Deserialize, Debug)]
787pub struct SnapshotFromDatasetResult {
788 #[serde(alias = "snapshot_id")]
790 pub id: SnapshotID,
791 #[serde(default)]
793 pub task_id: Option<TaskID>,
794}
795
796#[derive(Deserialize)]
797pub struct Experiment {
798 id: ExperimentID,
799 project_id: ProjectID,
800 name: String,
801 description: String,
802}
803
804impl Display for Experiment {
805 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
806 write!(f, "{} {}", self.id, self.name)
807 }
808}
809
810impl Experiment {
811 pub fn id(&self) -> ExperimentID {
812 self.id
813 }
814
815 pub fn project_id(&self) -> ProjectID {
816 self.project_id
817 }
818
819 pub fn name(&self) -> &str {
820 &self.name
821 }
822
823 pub fn description(&self) -> &str {
824 &self.description
825 }
826
827 pub async fn project(&self, client: &client::Client) -> Result<Project, Error> {
828 client.project(self.project_id).await
829 }
830
831 pub async fn training_sessions(
832 &self,
833 client: &client::Client,
834 name: Option<&str>,
835 ) -> Result<Vec<TrainingSession>, Error> {
836 client.training_sessions(self.id, name).await
837 }
838}
839
840#[derive(Serialize, Debug)]
841pub struct PublishMetrics {
842 #[serde(rename = "trainer_session_id", skip_serializing_if = "Option::is_none")]
843 pub trainer_session_id: Option<TrainingSessionID>,
844 #[serde(
845 rename = "validate_session_id",
846 skip_serializing_if = "Option::is_none"
847 )]
848 pub validate_session_id: Option<ValidationSessionID>,
849 pub metrics: HashMap<String, Parameter>,
850}
851
852#[derive(Deserialize)]
853struct TrainingSessionParams {
854 model_params: HashMap<String, Parameter>,
855 dataset_params: DatasetParams,
856}
857
858#[derive(Deserialize)]
859pub struct TrainingSession {
860 id: TrainingSessionID,
861 #[serde(rename = "trainer_id")]
862 experiment_id: ExperimentID,
863 model: String,
864 name: String,
865 description: String,
866 params: TrainingSessionParams,
867 #[serde(rename = "docker_task")]
868 task: Task,
869}
870
871impl Display for TrainingSession {
872 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
873 write!(f, "{} {}", self.id, self.name())
874 }
875}
876
877impl TrainingSession {
878 pub fn id(&self) -> TrainingSessionID {
879 self.id
880 }
881
882 pub fn name(&self) -> &str {
883 &self.name
884 }
885
886 pub fn description(&self) -> &str {
887 &self.description
888 }
889
890 pub fn model(&self) -> &str {
891 &self.model
892 }
893
894 pub fn experiment_id(&self) -> ExperimentID {
895 self.experiment_id
896 }
897
898 pub fn task(&self) -> Task {
899 self.task.clone()
900 }
901
902 pub fn model_params(&self) -> &HashMap<String, Parameter> {
903 &self.params.model_params
904 }
905
906 pub fn dataset_params(&self) -> &DatasetParams {
907 &self.params.dataset_params
908 }
909
910 pub fn train_group(&self) -> &str {
911 &self.params.dataset_params.train_group
912 }
913
914 pub fn val_group(&self) -> &str {
915 &self.params.dataset_params.val_group
916 }
917
918 pub async fn experiment(&self, client: &client::Client) -> Result<Experiment, Error> {
919 client.experiment(self.experiment_id).await
920 }
921
922 pub async fn dataset(&self, client: &client::Client) -> Result<Dataset, Error> {
923 client.dataset(self.params.dataset_params.dataset_id).await
924 }
925
926 pub async fn annotation_set(&self, client: &client::Client) -> Result<AnnotationSet, Error> {
927 client
928 .annotation_set(self.params.dataset_params.annotation_set_id)
929 .await
930 }
931
932 pub async fn artifacts(&self, client: &client::Client) -> Result<Vec<Artifact>, Error> {
933 client.artifacts(self.id).await
934 }
935
936 pub async fn metrics(
937 &self,
938 client: &client::Client,
939 ) -> Result<HashMap<String, Parameter>, Error> {
940 #[derive(Deserialize)]
941 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
942 enum Response {
943 Empty {},
944 Map(HashMap<String, Parameter>),
945 String(String),
946 }
947
948 let params = HashMap::from([("trainer_session_id", self.id().value())]);
949 let resp: Response = client
950 .rpc("trainer.session.metrics".to_owned(), Some(params))
951 .await?;
952
953 Ok(match resp {
954 Response::String(metrics) => serde_json::from_str(&metrics)?,
955 Response::Map(metrics) => metrics,
956 Response::Empty {} => HashMap::new(),
957 })
958 }
959
960 pub async fn set_metrics(
961 &self,
962 client: &client::Client,
963 metrics: HashMap<String, Parameter>,
964 ) -> Result<(), Error> {
965 let metrics = PublishMetrics {
966 trainer_session_id: Some(self.id()),
967 validate_session_id: None,
968 metrics,
969 };
970
971 let _: String = client
972 .rpc("trainer.session.metrics".to_owned(), Some(metrics))
973 .await?;
974
975 Ok(())
976 }
977
978 pub async fn download_artifact(
980 &self,
981 client: &client::Client,
982 filename: &str,
983 ) -> Result<Vec<u8>, Error> {
984 client
985 .fetch(&format!(
986 "download_model?training_session_id={}&file={}",
987 self.id().value(),
988 filename
989 ))
990 .await
991 }
992
993 pub async fn upload_artifact(
997 &self,
998 client: &client::Client,
999 filename: &str,
1000 path: PathBuf,
1001 ) -> Result<(), Error> {
1002 self.upload(client, &[(format!("artifacts/{}", filename), path)])
1003 .await
1004 }
1005
1006 pub async fn download_checkpoint(
1008 &self,
1009 client: &client::Client,
1010 filename: &str,
1011 ) -> Result<Vec<u8>, Error> {
1012 client
1013 .fetch(&format!(
1014 "download_checkpoint?folder=checkpoints&training_session_id={}&file={}",
1015 self.id().value(),
1016 filename
1017 ))
1018 .await
1019 }
1020
1021 pub async fn upload_checkpoint(
1025 &self,
1026 client: &client::Client,
1027 filename: &str,
1028 path: PathBuf,
1029 ) -> Result<(), Error> {
1030 self.upload(client, &[(format!("checkpoints/{}", filename), path)])
1031 .await
1032 }
1033
1034 pub async fn download(&self, client: &client::Client, filename: &str) -> Result<String, Error> {
1038 #[derive(Serialize)]
1039 struct DownloadRequest {
1040 session_id: TrainingSessionID,
1041 file_path: String,
1042 }
1043
1044 let params = DownloadRequest {
1045 session_id: self.id(),
1046 file_path: filename.to_string(),
1047 };
1048
1049 client
1050 .rpc("trainer.download.file".to_owned(), Some(params))
1051 .await
1052 }
1053
1054 pub async fn upload(
1055 &self,
1056 client: &client::Client,
1057 files: &[(String, PathBuf)],
1058 ) -> Result<(), Error> {
1059 let mut parts = Form::new().part(
1060 "params",
1061 Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1062 );
1063
1064 for (name, path) in files {
1065 let file_part = Part::file(path).await?.file_name(name.to_owned());
1066 parts = parts.part("file", file_part);
1067 }
1068
1069 let result = client.post_multipart("trainer.upload.files", parts).await?;
1070 trace!("TrainingSession::upload: {:?}", result);
1071 Ok(())
1072 }
1073}
1074
1075#[derive(Deserialize, Clone, Debug)]
1076pub struct ValidationSession {
1077 id: ValidationSessionID,
1078 description: String,
1079 dataset_id: DatasetID,
1080 experiment_id: ExperimentID,
1081 training_session_id: TrainingSessionID,
1082 #[serde(rename = "gt_annotation_set_id")]
1083 annotation_set_id: AnnotationSetID,
1084 #[serde(deserialize_with = "validation_session_params")]
1085 params: HashMap<String, Parameter>,
1086 #[serde(rename = "docker_task")]
1087 task: Task,
1088}
1089
1090fn validation_session_params<'de, D>(
1091 deserializer: D,
1092) -> Result<HashMap<String, Parameter>, D::Error>
1093where
1094 D: Deserializer<'de>,
1095{
1096 #[derive(Deserialize)]
1097 struct ModelParams {
1098 validation: Option<HashMap<String, Parameter>>,
1099 }
1100
1101 #[derive(Deserialize)]
1102 struct ValidateParams {
1103 model: String,
1104 }
1105
1106 #[derive(Deserialize)]
1107 struct Params {
1108 model_params: ModelParams,
1109 validate_params: ValidateParams,
1110 }
1111
1112 let params = Params::deserialize(deserializer)?;
1113 let params = match params.model_params.validation {
1114 Some(mut map) => {
1115 map.insert(
1116 "model".to_string(),
1117 Parameter::String(params.validate_params.model),
1118 );
1119 map
1120 }
1121 None => HashMap::from([(
1122 "model".to_string(),
1123 Parameter::String(params.validate_params.model),
1124 )]),
1125 };
1126
1127 Ok(params)
1128}
1129
1130impl ValidationSession {
1131 pub fn id(&self) -> ValidationSessionID {
1132 self.id
1133 }
1134
1135 pub fn name(&self) -> &str {
1136 self.task.name()
1137 }
1138
1139 pub fn description(&self) -> &str {
1140 &self.description
1141 }
1142
1143 pub fn dataset_id(&self) -> DatasetID {
1144 self.dataset_id
1145 }
1146
1147 pub fn experiment_id(&self) -> ExperimentID {
1148 self.experiment_id
1149 }
1150
1151 pub fn training_session_id(&self) -> TrainingSessionID {
1152 self.training_session_id
1153 }
1154
1155 pub fn annotation_set_id(&self) -> AnnotationSetID {
1156 self.annotation_set_id
1157 }
1158
1159 pub fn params(&self) -> &HashMap<String, Parameter> {
1160 &self.params
1161 }
1162
1163 pub fn task(&self) -> &Task {
1164 &self.task
1165 }
1166
1167 pub async fn metrics(
1168 &self,
1169 client: &client::Client,
1170 ) -> Result<HashMap<String, Parameter>, Error> {
1171 #[derive(Deserialize)]
1172 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1173 enum Response {
1174 Empty {},
1175 Map(HashMap<String, Parameter>),
1176 String(String),
1177 }
1178
1179 let params = HashMap::from([("validate_session_id", self.id().value())]);
1180 let resp: Response = client
1181 .rpc("validate.session.metrics".to_owned(), Some(params))
1182 .await?;
1183
1184 Ok(match resp {
1185 Response::String(metrics) => serde_json::from_str(&metrics)?,
1186 Response::Map(metrics) => metrics,
1187 Response::Empty {} => HashMap::new(),
1188 })
1189 }
1190
1191 pub async fn set_metrics(
1192 &self,
1193 client: &client::Client,
1194 metrics: HashMap<String, Parameter>,
1195 ) -> Result<(), Error> {
1196 let metrics = PublishMetrics {
1197 trainer_session_id: None,
1198 validate_session_id: Some(self.id()),
1199 metrics,
1200 };
1201
1202 let _: String = client
1203 .rpc("validate.session.metrics".to_owned(), Some(metrics))
1204 .await?;
1205
1206 Ok(())
1207 }
1208
1209 pub async fn upload_data(
1234 &self,
1235 client: &client::Client,
1236 files: &[(String, std::path::PathBuf)],
1237 folder: Option<&str>,
1238 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1239 ) -> Result<(), Error> {
1240 use futures::StreamExt;
1241 use std::sync::{
1242 Arc,
1243 atomic::{AtomicUsize, Ordering},
1244 };
1245 use tokio_util::io::ReaderStream;
1246
1247 let mut total: usize = 0;
1249 let mut file_meta = Vec::with_capacity(files.len());
1250 for (name, path) in files {
1251 let f = tokio::fs::File::open(path).await?;
1252 let len = f.metadata().await?.len() as usize;
1253 total += len;
1254 file_meta.push((name.clone(), f, len));
1255 }
1256
1257 let sent = Arc::new(AtomicUsize::new(0));
1259
1260 let mut form = Form::new().text("session_id", self.id().value().to_string());
1261 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
1262 form = form.text("folder", folder.to_owned());
1263 }
1264
1265 for (name, file, len) in file_meta {
1266 let reader_stream = ReaderStream::new(file);
1267 let sent_clone = sent.clone();
1268 let progress_clone = progress.clone();
1269 let progress_stream = reader_stream.inspect(move |chunk_result| {
1270 if let Ok(chunk) = chunk_result {
1271 let current =
1272 sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1273 if let Some(tx) = &progress_clone {
1278 let _ = tx.try_send(Progress {
1279 current,
1280 total,
1281 status: None,
1282 });
1283 }
1284 }
1285 });
1286 let body = reqwest::Body::wrap_stream(progress_stream);
1287 let part = Part::stream_with_length(body, len as u64).file_name(name);
1288 form = form.part("file", part);
1289 }
1290
1291 let result = match client.post_multipart("val.data.upload", form).await {
1292 Ok(_) => Ok(()),
1293 Err(Error::RpcError(code, msg)) => {
1294 Err(client::map_rpc_error("val.data.upload", code, msg, None))
1295 }
1296 Err(e) => Err(e),
1297 };
1298
1299 if result.is_ok()
1304 && let Some(tx) = progress
1305 {
1306 let _ = tx
1307 .send(Progress {
1308 current: total,
1309 total,
1310 status: None,
1311 })
1312 .await;
1313 }
1314 result
1315 }
1316
1317 pub async fn download_data(
1337 &self,
1338 client: &client::Client,
1339 filename: &str,
1340 output_path: &std::path::Path,
1341 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1342 ) -> Result<(), Error> {
1343 let req = client::ValDataDownloadRequest {
1344 session_id: self.id().value(),
1345 filename: filename.to_owned(),
1346 };
1347 match client
1348 .rpc_download("val.data.download", &req, output_path, progress)
1349 .await
1350 {
1351 Ok(()) => Ok(()),
1352 Err(Error::RpcError(code, msg)) => {
1353 Err(client::map_rpc_error("val.data.download", code, msg, None))
1354 }
1355 Err(e) => Err(e),
1356 }
1357 }
1358
1359 pub async fn data_list(&self, client: &client::Client) -> Result<Vec<String>, Error> {
1374 let req = client::ValDataListRequest {
1375 session_id: self.id().value(),
1376 };
1377 match client.rpc("val.data.list".to_owned(), Some(&req)).await {
1378 Ok(r) => Ok(r),
1379 Err(Error::RpcError(code, msg)) => {
1380 Err(client::map_rpc_error("val.data.list", code, msg, None))
1381 }
1382 Err(e) => Err(e),
1383 }
1384 }
1385}
1386
1387#[derive(Debug, Clone)]
1406pub struct StartValidationRequest {
1407 pub project_id: ProjectID,
1408 pub name: String,
1409 pub training_session_id: TrainingSessionID,
1410 pub model_file: String,
1411 pub val_type: String,
1412 pub params: HashMap<String, Parameter>,
1413 pub is_local: bool,
1414 pub is_kubernetes: bool,
1415 pub description: Option<String>,
1416 pub dataset_id: Option<DatasetID>,
1417 pub annotation_set_id: Option<AnnotationSetID>,
1418 pub snapshot_id: Option<SnapshotID>,
1419}
1420
1421#[derive(Deserialize, Debug, Clone)]
1436pub struct NewValidationSession {
1437 #[serde(rename = "id")]
1438 pub task_id: TaskID,
1439 #[serde(rename = "val_session_id", default)]
1440 pub session_id: Option<ValidationSessionID>,
1441}
1442
1443impl Display for NewValidationSession {
1444 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1445 match self.session_id {
1446 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1447 None => write!(f, "task {} (no session)", self.task_id),
1448 }
1449 }
1450}
1451
1452#[derive(Debug, Clone)]
1472pub struct StartTrainingRequest {
1473 pub project_id: ProjectID,
1475 pub name: String,
1477 pub experiment_id: ExperimentID,
1479 pub trainer_type: String,
1482 pub dataset_id: DatasetID,
1484 pub annotation_set_id: AnnotationSetID,
1486 pub tag_name: Option<String>,
1490 pub train_group: Option<String>,
1492 pub val_group: Option<String>,
1494 pub session_name: Option<String>,
1497 pub session_description: Option<String>,
1499 pub weights_session: Option<TrainingSessionID>,
1501 pub params: HashMap<String, Parameter>,
1503 pub is_local: bool,
1505 pub is_kubernetes: bool,
1507}
1508
1509#[derive(Deserialize, Debug, Clone)]
1522pub struct NewTrainingSession {
1523 #[serde(rename = "id")]
1524 pub task_id: TaskID,
1525 #[serde(rename = "train_session_id", default)]
1526 pub session_id: Option<TrainingSessionID>,
1527}
1528
1529impl Display for NewTrainingSession {
1530 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1531 match self.session_id {
1532 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1533 None => write!(f, "task {} (no session)", self.task_id),
1534 }
1535 }
1536}
1537
1538#[derive(Deserialize, Debug, Clone)]
1546pub struct Tag {
1547 pub id: u64,
1549 pub name: String,
1551 #[serde(default)]
1553 pub dataset_id: u64,
1554}
1555
1556#[derive(Deserialize, Clone, Debug)]
1557pub struct DatasetParams {
1558 dataset_id: DatasetID,
1559 annotation_set_id: AnnotationSetID,
1560 #[serde(rename = "train_group_name")]
1561 train_group: String,
1562 #[serde(rename = "val_group_name")]
1563 val_group: String,
1564}
1565
1566impl DatasetParams {
1567 pub fn dataset_id(&self) -> DatasetID {
1568 self.dataset_id
1569 }
1570
1571 pub fn annotation_set_id(&self) -> AnnotationSetID {
1572 self.annotation_set_id
1573 }
1574
1575 pub fn train_group(&self) -> &str {
1576 &self.train_group
1577 }
1578
1579 pub fn val_group(&self) -> &str {
1580 &self.val_group
1581 }
1582}
1583
1584#[derive(Serialize, Debug, Clone)]
1585pub struct TasksListParams {
1586 #[serde(skip_serializing_if = "Option::is_none")]
1587 pub continue_token: Option<String>,
1588 #[serde(skip_serializing_if = "Option::is_none")]
1589 pub types: Option<Vec<String>>,
1590 #[serde(rename = "manage_types", skip_serializing_if = "Option::is_none")]
1591 pub manager: Option<Vec<String>>,
1592 #[serde(skip_serializing_if = "Option::is_none")]
1593 pub status: Option<Vec<String>>,
1594}
1595
1596#[derive(Debug, Clone, Serialize, Deserialize)]
1602pub struct TaskDataList {
1603 pub server: String,
1604 #[serde(rename = "organization_uid")]
1605 pub organization_uid: String,
1606 #[serde(default)]
1607 pub traces: Vec<String>,
1608 #[serde(default)]
1609 pub data: std::collections::HashMap<String, Vec<String>>,
1610}
1611
1612#[derive(Debug, Clone, Serialize, Deserialize)]
1617pub struct Job {
1618 #[serde(default)]
1620 pub code: String,
1621 #[serde(default)]
1623 pub title: String,
1624 #[serde(default)]
1626 pub job_name: String,
1627 #[serde(default)]
1629 pub job_id: String,
1630 #[serde(default)]
1632 pub state: String,
1633 #[serde(default)]
1635 pub launch: Option<DateTime<Utc>>,
1636 pub task_id: i64,
1641}
1642
1643impl Job {
1644 pub fn task_id(&self) -> TaskID {
1650 TaskID::from(self.task_id.max(0) as u64)
1651 }
1652}
1653
1654#[derive(Deserialize, Debug, Clone)]
1655pub struct TasksListResult {
1656 pub tasks: Vec<Task>,
1657 pub continue_token: Option<String>,
1658}
1659
1660#[derive(Deserialize, Debug, Clone)]
1661pub struct Task {
1662 id: TaskID,
1663 name: String,
1664 #[serde(rename = "type")]
1665 workflow: String,
1666 status: String,
1667 #[serde(rename = "manage_type")]
1668 manager: Option<String>,
1669 #[serde(rename = "instance_type")]
1670 instance: String,
1671 #[serde(rename = "date")]
1672 created: DateTime<Utc>,
1673}
1674
1675impl Task {
1676 pub fn id(&self) -> TaskID {
1677 self.id
1678 }
1679
1680 pub fn name(&self) -> &str {
1681 &self.name
1682 }
1683
1684 pub fn workflow(&self) -> &str {
1685 &self.workflow
1686 }
1687
1688 pub fn status(&self) -> &str {
1689 &self.status
1690 }
1691
1692 pub fn manager(&self) -> Option<&str> {
1693 self.manager.as_deref()
1694 }
1695
1696 pub fn instance(&self) -> &str {
1697 &self.instance
1698 }
1699
1700 pub fn created(&self) -> &DateTime<Utc> {
1701 &self.created
1702 }
1703}
1704
1705impl Display for Task {
1706 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1707 write!(
1708 f,
1709 "{} [{:?} {}] {}",
1710 self.id,
1711 self.manager(),
1712 self.workflow(),
1713 self.name()
1714 )
1715 }
1716}
1717
1718#[derive(Deserialize, Debug, Clone)]
1719pub struct TaskInfo {
1720 id: TaskID,
1721 project_id: Option<ProjectID>,
1722 #[serde(rename = "task_description", alias = "description", default)]
1723 description: String,
1724 #[serde(rename = "type")]
1725 workflow: String,
1726 status: Option<String>,
1727 #[serde(default)]
1728 progress: TaskProgress,
1729 #[serde(
1730 rename = "created_date",
1731 alias = "created",
1732 default = "default_datetime_utc"
1733 )]
1734 created: DateTime<Utc>,
1735 #[serde(
1736 rename = "end_date",
1737 alias = "completed",
1738 default = "default_datetime_utc"
1739 )]
1740 completed: DateTime<Utc>,
1741}
1742
1743fn default_datetime_utc() -> DateTime<Utc> {
1744 DateTime::UNIX_EPOCH
1745}
1746
1747impl Display for TaskInfo {
1748 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1749 write!(f, "{} {}: {}", self.id, self.workflow(), self.description())
1750 }
1751}
1752
1753impl TaskInfo {
1754 pub fn id(&self) -> TaskID {
1755 self.id
1756 }
1757
1758 pub fn project_id(&self) -> Option<ProjectID> {
1759 self.project_id
1760 }
1761
1762 pub fn description(&self) -> &str {
1763 &self.description
1764 }
1765
1766 pub fn workflow(&self) -> &str {
1767 &self.workflow
1768 }
1769
1770 pub fn status(&self) -> &Option<String> {
1771 &self.status
1772 }
1773
1774 pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1775 let t = client.task_status(self.id(), status).await?;
1776 self.status = Some(t.status);
1777 Ok(())
1778 }
1779
1780 pub fn stages(&self) -> HashMap<String, Stage> {
1781 match &self.progress.stages {
1782 Some(stages) => stages.clone(),
1783 None => HashMap::new(),
1784 }
1785 }
1786
1787 pub async fn update_stage(
1788 &mut self,
1789 client: &Client,
1790 stage: &str,
1791 status: &str,
1792 message: &str,
1793 percentage: u8,
1794 ) -> Result<(), Error> {
1795 client
1796 .update_stage(self.id(), stage, status, message, percentage)
1797 .await?;
1798 let t = client.task_info(self.id()).await?;
1799 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1800 Ok(())
1801 }
1802
1803 pub async fn set_stages(
1804 &mut self,
1805 client: &Client,
1806 stages: &[(&str, &str)],
1807 ) -> Result<(), Error> {
1808 client.set_stages(self.id(), stages).await?;
1809 let t = client.task_info(self.id()).await?;
1810 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1811 Ok(())
1812 }
1813
1814 pub async fn data_list(&self, client: &client::Client) -> Result<TaskDataList, Error> {
1830 let req = client::TaskDataListRequest {
1831 task_id: self.id().value(),
1832 };
1833 match client.rpc("task.data.list".to_owned(), Some(&req)).await {
1834 Ok(r) => Ok(r),
1835 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1836 "task.data.list",
1837 code,
1838 msg,
1839 Some(self.id()),
1840 )),
1841 Err(e) => Err(e),
1842 }
1843 }
1844
1845 pub async fn upload_data(
1866 &self,
1867 client: &client::Client,
1868 path: &std::path::Path,
1869 folder: Option<&str>,
1870 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1871 ) -> Result<(), Error> {
1872 use futures::StreamExt;
1873 use std::sync::{
1874 Arc,
1875 atomic::{AtomicUsize, Ordering},
1876 };
1877 use tokio_util::io::ReaderStream;
1878
1879 let file_name = path
1880 .file_name()
1881 .and_then(|s| s.to_str())
1882 .ok_or_else(|| Error::InvalidParameters("path must have a UTF-8 filename".into()))?
1883 .to_owned();
1884
1885 let file = tokio::fs::File::open(path).await?;
1886 let total = file.metadata().await?.len() as usize;
1887 let sent = Arc::new(AtomicUsize::new(0));
1888
1889 let reader_stream = ReaderStream::new(file);
1890 let sent_clone = sent.clone();
1891 let progress_clone = progress.clone();
1892 let progress_stream = reader_stream.inspect(move |chunk_result| {
1893 if let Ok(chunk) = chunk_result {
1894 let current = sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1895 if let Some(tx) = &progress_clone {
1901 let _ = tx.try_send(Progress {
1902 current,
1903 total,
1904 status: None,
1905 });
1906 }
1907 }
1908 });
1909
1910 let body = reqwest::Body::wrap_stream(progress_stream);
1911 let file_part = Part::stream_with_length(body, total as u64).file_name(file_name);
1912
1913 let mut form = Form::new().text("task_id", self.id().value().to_string());
1914 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
1915 form = form.text("folder", folder.to_owned());
1916 }
1917 form = form.part("file", file_part);
1918
1919 let result = match client.post_multipart("task.data.upload", form).await {
1920 Ok(_) => Ok(()),
1921 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1922 "task.data.upload",
1923 code,
1924 msg,
1925 Some(self.id()),
1926 )),
1927 Err(e) => Err(e),
1928 };
1929
1930 if result.is_ok()
1934 && let Some(tx) = progress
1935 {
1936 let _ = tx
1937 .send(Progress {
1938 current: total,
1939 total,
1940 status: None,
1941 })
1942 .await;
1943 }
1944 result
1945 }
1946
1947 pub async fn download_data(
1976 &self,
1977 client: &client::Client,
1978 file: &str,
1979 folder: Option<&str>,
1980 output_path: &std::path::Path,
1981 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1982 ) -> Result<(), Error> {
1983 let folder = folder.unwrap_or("").to_owned();
1984 let req = client::TaskDataDownloadRequest {
1985 task_id: self.id().value(),
1986 folder,
1987 file: file.to_owned(),
1988 };
1989 match client
1990 .rpc_download("task.data.download", &req, output_path, progress)
1991 .await
1992 {
1993 Ok(()) => Ok(()),
1994 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1995 "task.data.download",
1996 code,
1997 msg,
1998 Some(self.id()),
1999 )),
2000 Err(e) => Err(e),
2001 }
2002 }
2003
2004 pub async fn add_chart(
2032 &self,
2033 client: &client::Client,
2034 group: &str,
2035 name: &str,
2036 data: Parameter,
2037 params: Option<Parameter>,
2038 ) -> Result<(), Error> {
2039 client::validate_chart_args(group, name)?;
2040 let req = client::TaskChartAddRequest {
2041 task_id: self.id().value(),
2042 group_name: group.to_owned(),
2043 chart_name: name.to_owned(),
2044 params,
2045 data,
2046 };
2047 let _resp: serde_json::Value =
2048 match client.rpc("task.chart.add".to_owned(), Some(&req)).await {
2049 Ok(r) => r,
2050 Err(Error::RpcError(code, msg)) => {
2051 return Err(client::map_rpc_error(
2052 "task.chart.add",
2053 code,
2054 msg,
2055 Some(self.id()),
2056 ));
2057 }
2058 Err(e) => return Err(e),
2059 };
2060 Ok(())
2061 }
2062
2063 pub async fn list_charts(
2080 &self,
2081 client: &client::Client,
2082 group: Option<&str>,
2083 ) -> Result<TaskDataList, Error> {
2084 let req = client::TaskChartListRequest {
2085 task_id: self.id().value(),
2086 group_name: group.unwrap_or("").to_owned(),
2087 };
2088 match client.rpc("task.chart.list".to_owned(), Some(&req)).await {
2089 Ok(r) => Ok(r),
2090 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2091 "task.chart.list",
2092 code,
2093 msg,
2094 Some(self.id()),
2095 )),
2096 Err(e) => Err(e),
2097 }
2098 }
2099
2100 pub async fn get_chart(
2119 &self,
2120 client: &client::Client,
2121 group: &str,
2122 name: &str,
2123 ) -> Result<Parameter, Error> {
2124 client::validate_chart_args(group, name)?;
2125 let req = client::TaskChartGetRequest {
2126 task_id: self.id().value(),
2127 group_name: group.to_owned(),
2128 chart_name: name.to_owned(),
2129 };
2130 match client.rpc("task.chart.get".to_owned(), Some(&req)).await {
2131 Ok(r) => Ok(r),
2132 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2133 "task.chart.get",
2134 code,
2135 msg,
2136 Some(self.id()),
2137 )),
2138 Err(e) => Err(e),
2139 }
2140 }
2141
2142 pub fn created(&self) -> &DateTime<Utc> {
2143 &self.created
2144 }
2145
2146 pub fn completed(&self) -> &DateTime<Utc> {
2147 &self.completed
2148 }
2149}
2150
2151#[derive(Deserialize, Debug, Default, Clone)]
2152pub struct TaskProgress {
2153 stages: Option<HashMap<String, Stage>>,
2154}
2155
2156#[derive(Serialize, Debug, Clone)]
2157pub struct TaskStatus {
2158 #[serde(rename = "docker_task_id")]
2159 pub task_id: TaskID,
2160 pub status: String,
2161}
2162
2163#[derive(Serialize, Deserialize, Debug, Clone)]
2164pub struct Stage {
2165 #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
2166 task_id: Option<TaskID>,
2167 stage: String,
2168 #[serde(skip_serializing_if = "Option::is_none")]
2169 status: Option<String>,
2170 #[serde(skip_serializing_if = "Option::is_none")]
2171 description: Option<String>,
2172 #[serde(skip_serializing_if = "Option::is_none")]
2173 message: Option<String>,
2174 percentage: u8,
2175}
2176
2177impl Stage {
2178 pub fn new(
2179 task_id: Option<TaskID>,
2180 stage: String,
2181 status: Option<String>,
2182 message: Option<String>,
2183 percentage: u8,
2184 ) -> Self {
2185 Stage {
2186 task_id,
2187 stage,
2188 status,
2189 description: None,
2190 message,
2191 percentage,
2192 }
2193 }
2194
2195 pub fn task_id(&self) -> &Option<TaskID> {
2196 &self.task_id
2197 }
2198
2199 pub fn stage(&self) -> &str {
2200 &self.stage
2201 }
2202
2203 pub fn status(&self) -> &Option<String> {
2204 &self.status
2205 }
2206
2207 pub fn description(&self) -> &Option<String> {
2208 &self.description
2209 }
2210
2211 pub fn message(&self) -> &Option<String> {
2212 &self.message
2213 }
2214
2215 pub fn percentage(&self) -> u8 {
2216 self.percentage
2217 }
2218}
2219
2220#[derive(Serialize, Debug)]
2221pub struct TaskStages {
2222 #[serde(rename = "docker_task_id")]
2223 pub task_id: TaskID,
2224 #[serde(skip_serializing_if = "Vec::is_empty")]
2225 pub stages: Vec<HashMap<String, String>>,
2226}
2227
2228#[derive(Deserialize, Debug)]
2229pub struct Artifact {
2230 name: String,
2231 #[serde(rename = "modelType")]
2232 model_type: String,
2233}
2234
2235impl Artifact {
2236 pub fn name(&self) -> &str {
2237 &self.name
2238 }
2239
2240 pub fn model_type(&self) -> &str {
2241 &self.model_type
2242 }
2243}
2244
2245#[derive(Deserialize, Serialize, Clone, Debug)]
2253pub struct VersionTag {
2254 id: u64,
2255 dataset_id: DatasetID,
2256 name: String,
2257 serial: u64,
2258 #[serde(default)]
2259 description: String,
2260 created_by: String,
2261 created_at: DateTime<Utc>,
2262 #[serde(default)]
2263 image_count: u64,
2264 #[serde(default)]
2265 annotation_counts: HashMap<String, u64>,
2266 #[serde(default)]
2267 sensor_counts: HashMap<String, u64>,
2268 #[serde(default)]
2269 label_count: u64,
2270 #[serde(default)]
2271 annotation_set_count: u64,
2272 #[serde(default)]
2273 snapshot_id: Option<u64>,
2274 #[serde(default)]
2275 is_current: bool,
2276}
2277
2278impl VersionTag {
2279 pub fn id(&self) -> u64 {
2281 self.id
2282 }
2283
2284 pub fn dataset_id(&self) -> DatasetID {
2286 self.dataset_id
2287 }
2288
2289 pub fn name(&self) -> &str {
2291 &self.name
2292 }
2293
2294 pub fn serial(&self) -> u64 {
2296 self.serial
2297 }
2298
2299 pub fn description(&self) -> &str {
2301 &self.description
2302 }
2303
2304 pub fn created_by(&self) -> &str {
2306 &self.created_by
2307 }
2308
2309 pub fn created_at(&self) -> DateTime<Utc> {
2311 self.created_at
2312 }
2313
2314 pub fn image_count(&self) -> u64 {
2316 self.image_count
2317 }
2318
2319 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2321 &self.annotation_counts
2322 }
2323
2324 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2326 &self.sensor_counts
2327 }
2328
2329 pub fn label_count(&self) -> u64 {
2331 self.label_count
2332 }
2333
2334 pub fn annotation_set_count(&self) -> u64 {
2336 self.annotation_set_count
2337 }
2338
2339 pub fn snapshot_id(&self) -> Option<u64> {
2341 self.snapshot_id
2342 }
2343
2344 pub fn is_current(&self) -> bool {
2347 self.is_current
2348 }
2349}
2350
2351impl Display for VersionTag {
2352 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2353 write!(f, "{} (serial {})", self.name, self.serial)
2354 }
2355}
2356
2357#[derive(Deserialize, Serialize, Clone, Debug)]
2359pub struct ChangelogEntry {
2360 id: u64,
2361 dataset_id: DatasetID,
2362 serial: u64,
2363 entity_type: String,
2364 operation: String,
2365 #[serde(default)]
2366 entity_id: Option<u64>,
2367 #[serde(default)]
2368 change_data: serde_json::Value,
2369 username: String,
2370 organization_id: u64,
2371 created_at: DateTime<Utc>,
2372 #[serde(default)]
2373 message: String,
2374 #[serde(default, deserialize_with = "deserialize_null_as_default")]
2375 s3_version_ids: Vec<serde_json::Value>,
2376}
2377
2378impl ChangelogEntry {
2379 pub fn id(&self) -> u64 {
2380 self.id
2381 }
2382
2383 pub fn dataset_id(&self) -> DatasetID {
2384 self.dataset_id
2385 }
2386
2387 pub fn serial(&self) -> u64 {
2389 self.serial
2390 }
2391
2392 pub fn entity_type(&self) -> &str {
2394 &self.entity_type
2395 }
2396
2397 pub fn operation(&self) -> &str {
2399 &self.operation
2400 }
2401
2402 pub fn entity_id(&self) -> Option<u64> {
2403 self.entity_id
2404 }
2405
2406 pub fn change_data(&self) -> &serde_json::Value {
2408 &self.change_data
2409 }
2410
2411 pub fn username(&self) -> &str {
2412 &self.username
2413 }
2414
2415 pub fn organization_id(&self) -> u64 {
2416 self.organization_id
2417 }
2418
2419 pub fn created_at(&self) -> DateTime<Utc> {
2420 self.created_at
2421 }
2422
2423 pub fn message(&self) -> &str {
2424 &self.message
2425 }
2426
2427 pub fn s3_version_ids(&self) -> &[serde_json::Value] {
2428 &self.s3_version_ids
2429 }
2430}
2431
2432#[derive(Deserialize, Debug, Clone)]
2434pub struct ChangelogResponse {
2435 pub entries: Vec<ChangelogEntry>,
2436 pub count: u64,
2437 #[serde(default)]
2438 pub continue_token: String,
2439 #[serde(default)]
2440 pub from_serial: Option<u64>,
2441 #[serde(default)]
2442 pub to_serial: Option<u64>,
2443}
2444
2445#[derive(Deserialize, Serialize, Clone, Debug)]
2447pub struct DatasetSummary {
2448 dataset_id: DatasetID,
2449 current_serial: u64,
2450 #[serde(default)]
2451 image_count: u64,
2452 #[serde(default)]
2453 annotation_counts: HashMap<String, u64>,
2454 #[serde(default)]
2455 sensor_counts: HashMap<String, u64>,
2456 #[serde(default)]
2457 label_count: u64,
2458 #[serde(default)]
2459 annotation_set_count: u64,
2460 last_updated: DateTime<Utc>,
2461}
2462
2463impl DatasetSummary {
2464 pub fn dataset_id(&self) -> DatasetID {
2465 self.dataset_id
2466 }
2467
2468 pub fn current_serial(&self) -> u64 {
2469 self.current_serial
2470 }
2471
2472 pub fn image_count(&self) -> u64 {
2473 self.image_count
2474 }
2475
2476 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2477 &self.annotation_counts
2478 }
2479
2480 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2481 &self.sensor_counts
2482 }
2483
2484 pub fn label_count(&self) -> u64 {
2485 self.label_count
2486 }
2487
2488 pub fn annotation_set_count(&self) -> u64 {
2489 self.annotation_set_count
2490 }
2491
2492 pub fn last_updated(&self) -> DateTime<Utc> {
2493 self.last_updated
2494 }
2495}
2496
2497#[derive(Deserialize, Debug, Clone)]
2499pub struct VersionCurrentResponse {
2500 pub dataset_id: DatasetID,
2501 pub current_serial: u64,
2502 #[serde(default)]
2503 pub latest_tag: Option<VersionTag>,
2504 #[serde(default)]
2505 pub tags: Vec<VersionTag>,
2506 #[serde(default)]
2507 pub summary: Option<DatasetSummary>,
2508}
2509
2510#[derive(Deserialize, Debug, Clone)]
2512pub struct RestoredFrom {
2513 pub tag: String,
2514 pub serial: u64,
2515}
2516
2517#[derive(Deserialize, Debug, Clone)]
2519pub struct RestoredCounts {
2520 pub images: u64,
2521 pub labels: u64,
2522 pub annotation_sets: u64,
2523}
2524
2525#[derive(Deserialize, Debug, Clone)]
2527pub struct RestoreResult {
2528 pub success: bool,
2529 pub new_serial: u64,
2530 pub restored_from: RestoredFrom,
2531 pub restored_counts: RestoredCounts,
2532 pub message: String,
2533}
2534
2535#[derive(Serialize)]
2538pub(crate) struct VersionTagCreateParams {
2539 pub dataset_id: DatasetID,
2540 pub name: String,
2541 #[serde(skip_serializing_if = "Option::is_none")]
2542 pub description: Option<String>,
2543}
2544
2545#[derive(Serialize)]
2546pub(crate) struct VersionTagNameParams {
2547 pub dataset_id: DatasetID,
2548 pub name: String,
2549}
2550
2551#[derive(Serialize)]
2552pub(crate) struct VersionChangelogParams {
2553 pub dataset_id: DatasetID,
2554 #[serde(skip_serializing_if = "Option::is_none")]
2555 pub from_version: Option<String>,
2556 #[serde(skip_serializing_if = "Option::is_none")]
2557 pub to_version: Option<String>,
2558 #[serde(skip_serializing_if = "Option::is_none")]
2559 pub entity_types: Option<Vec<String>>,
2560 #[serde(skip_serializing_if = "Option::is_none")]
2561 pub limit: Option<u64>,
2562 #[serde(skip_serializing_if = "Option::is_none")]
2563 pub continue_token: Option<String>,
2564}
2565
2566#[derive(Deserialize, Debug)]
2568pub(crate) struct ChangelogCountResult {
2569 pub count: u64,
2570}
2571
2572#[derive(Serialize, Deserialize, Debug, Clone)]
2579pub struct TrainerSchemaInfo {
2580 pub name: String,
2582 #[serde(default)]
2584 pub label: String,
2585 #[serde(default)]
2587 pub schema_type: String,
2588}
2589
2590#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
2596#[serde(rename_all = "lowercase")]
2597pub enum SchemaFieldType {
2598 Group,
2600 Slider,
2602 Select,
2604 Bool,
2606 Int,
2608 Float,
2610 Text,
2612 Date,
2614 Project,
2616 Dataset,
2618 Trainer,
2620 Upload,
2622 Info,
2625 #[serde(other)]
2627 Unknown,
2628}
2629
2630fn lenient_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
2634where
2635 D: Deserializer<'de>,
2636{
2637 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
2638 Ok(value.map(|v| match v {
2639 serde_json::Value::String(s) => s,
2640 other => other.to_string(),
2641 }))
2642}
2643
2644#[derive(Serialize, Deserialize, Debug, Clone)]
2646pub struct SchemaOption {
2647 #[serde(default)]
2649 pub name: Option<Parameter>,
2650 #[serde(default, deserialize_with = "lenient_string")]
2653 pub label: Option<String>,
2654 #[serde(default)]
2656 pub children: Vec<SchemaField>,
2657}
2658
2659#[derive(Serialize, Deserialize, Debug, Clone)]
2671pub struct SchemaField {
2672 #[serde(default, deserialize_with = "lenient_string")]
2674 pub name: Option<String>,
2675 #[serde(default, deserialize_with = "lenient_string")]
2677 pub label: Option<String>,
2678 #[serde(default, deserialize_with = "lenient_string")]
2680 pub description: Option<String>,
2681 #[serde(default)]
2683 pub required: bool,
2684 #[serde(default)]
2686 pub default: Option<Parameter>,
2687 #[serde(rename = "type", default)]
2689 pub field_type: Option<SchemaFieldType>,
2690 #[serde(default)]
2692 pub min: Option<f64>,
2693 #[serde(default)]
2695 pub max: Option<f64>,
2696 #[serde(default)]
2698 pub step: Option<f64>,
2699 #[serde(default)]
2701 pub options: Vec<SchemaOption>,
2702 #[serde(default)]
2705 pub children: Vec<SchemaField>,
2706 #[serde(default)]
2708 pub is_dropdown: bool,
2709 #[serde(default)]
2711 pub multi_select: bool,
2712 #[serde(default)]
2714 pub is_multi_line: bool,
2715 #[serde(default)]
2717 pub hidden: bool,
2718 #[serde(default)]
2720 pub numeric_only: bool,
2721 #[serde(default)]
2723 pub enable_tags_selection: bool,
2724 #[serde(default)]
2726 pub enable_annotation_set_selection: bool,
2727 #[serde(default)]
2729 pub values: Option<Vec<Parameter>>,
2730}
2731
2732#[derive(Serialize, Deserialize, Debug, Clone)]
2735pub struct ValidatorSchema {
2736 #[serde(rename = "type", default)]
2738 pub schema_type: String,
2739 #[serde(default)]
2741 pub name: String,
2742 #[serde(default)]
2744 pub schema: Vec<SchemaField>,
2745}
2746
2747#[cfg(test)]
2748mod tests {
2749 use super::*;
2750
2751 #[test]
2753 fn test_organization_id_from_u64() {
2754 let id = OrganizationID::from(12345);
2755 assert_eq!(id.value(), 12345);
2756 }
2757
2758 #[test]
2759 fn test_organization_id_display() {
2760 let id = OrganizationID::from(0xabc123);
2761 assert_eq!(format!("{}", id), "org-abc123");
2762 }
2763
2764 #[test]
2765 fn test_organization_id_try_from_str_valid() {
2766 let id = OrganizationID::try_from("org-abc123").unwrap();
2767 assert_eq!(id.value(), 0xabc123);
2768 }
2769
2770 #[test]
2771 fn test_organization_id_try_from_str_invalid_prefix() {
2772 let result = OrganizationID::try_from("invalid-abc123");
2773 assert!(result.is_err());
2774 match result {
2775 Err(Error::InvalidParameters(msg)) => {
2776 assert!(msg.contains("must start with 'org-'"));
2777 }
2778 _ => panic!("Expected InvalidParameters error"),
2779 }
2780 }
2781
2782 #[test]
2783 fn test_organization_id_try_from_str_invalid_hex() {
2784 let result = OrganizationID::try_from("org-xyz");
2785 assert!(result.is_err());
2786 }
2787
2788 #[test]
2789 fn test_organization_id_try_from_str_empty() {
2790 let result = OrganizationID::try_from("org-");
2791 assert!(result.is_err());
2792 }
2793
2794 #[test]
2795 fn test_organization_id_into_u64() {
2796 let id = OrganizationID::from(54321);
2797 let value: u64 = id.into();
2798 assert_eq!(value, 54321);
2799 }
2800
2801 #[test]
2803 fn test_usage_summary_deserialize_and_accessors() {
2804 let usage: UsageSummary = serde_json::from_str(
2805 r#"{"credits": 12.5, "funds": 49092.92, "total_funds_and_credits": 49105.42}"#,
2806 )
2807 .unwrap();
2808 assert_eq!(usage.credits(), 12.5);
2809 assert_eq!(usage.funds(), 49092.92);
2810 assert_eq!(usage.total(), 49105.42);
2811 }
2812
2813 #[test]
2814 fn test_usage_summary_defaults_for_missing_fields() {
2815 let usage: UsageSummary = serde_json::from_str("{}").unwrap();
2819 assert_eq!(usage.credits(), 0.0);
2820 assert_eq!(usage.funds(), 0.0);
2821 assert_eq!(usage.total(), 0.0);
2822 }
2823
2824 #[test]
2826 fn test_project_id_from_u64() {
2827 let id = ProjectID::from(78910);
2828 assert_eq!(id.value(), 78910);
2829 }
2830
2831 #[test]
2832 fn test_project_id_display() {
2833 let id = ProjectID::from(0xdef456);
2834 assert_eq!(format!("{}", id), "p-def456");
2835 }
2836
2837 #[test]
2838 fn test_project_id_from_str_valid() {
2839 let id = ProjectID::from_str("p-def456").unwrap();
2840 assert_eq!(id.value(), 0xdef456);
2841 }
2842
2843 #[test]
2844 fn test_project_id_try_from_str_valid() {
2845 let id = ProjectID::try_from("p-123abc").unwrap();
2846 assert_eq!(id.value(), 0x123abc);
2847 }
2848
2849 #[test]
2850 fn test_project_id_try_from_string_valid() {
2851 let id = ProjectID::try_from("p-456def".to_string()).unwrap();
2852 assert_eq!(id.value(), 0x456def);
2853 }
2854
2855 #[test]
2856 fn test_project_id_from_str_invalid_prefix() {
2857 let result = ProjectID::from_str("proj-123");
2858 assert!(result.is_err());
2859 match result {
2860 Err(Error::InvalidParameters(msg)) => {
2861 assert!(msg.contains("must start with 'p-'"));
2862 }
2863 _ => panic!("Expected InvalidParameters error"),
2864 }
2865 }
2866
2867 #[test]
2868 fn test_project_id_from_str_invalid_hex() {
2869 let result = ProjectID::from_str("p-notahex");
2870 assert!(result.is_err());
2871 }
2872
2873 #[test]
2874 fn test_project_id_into_u64() {
2875 let id = ProjectID::from(99999);
2876 let value: u64 = id.into();
2877 assert_eq!(value, 99999);
2878 }
2879
2880 #[test]
2882 fn test_experiment_id_from_u64() {
2883 let id = ExperimentID::from(1193046);
2884 assert_eq!(id.value(), 1193046);
2885 }
2886
2887 #[test]
2888 fn test_experiment_id_display() {
2889 let id = ExperimentID::from(0x123abc);
2890 assert_eq!(format!("{}", id), "exp-123abc");
2891 }
2892
2893 #[test]
2894 fn test_experiment_id_from_str_valid() {
2895 let id = ExperimentID::from_str("exp-456def").unwrap();
2896 assert_eq!(id.value(), 0x456def);
2897 }
2898
2899 #[test]
2900 fn test_experiment_id_try_from_str_valid() {
2901 let id = ExperimentID::try_from("exp-789abc").unwrap();
2902 assert_eq!(id.value(), 0x789abc);
2903 }
2904
2905 #[test]
2906 fn test_experiment_id_try_from_string_valid() {
2907 let id = ExperimentID::try_from("exp-fedcba".to_string()).unwrap();
2908 assert_eq!(id.value(), 0xfedcba);
2909 }
2910
2911 #[test]
2912 fn test_experiment_id_from_str_invalid_prefix() {
2913 let result = ExperimentID::from_str("experiment-123");
2914 assert!(result.is_err());
2915 match result {
2916 Err(Error::InvalidParameters(msg)) => {
2917 assert!(msg.contains("must start with 'exp-'"));
2918 }
2919 _ => panic!("Expected InvalidParameters error"),
2920 }
2921 }
2922
2923 #[test]
2924 fn test_experiment_id_from_str_invalid_hex() {
2925 let result = ExperimentID::from_str("exp-zzz");
2926 assert!(result.is_err());
2927 }
2928
2929 #[test]
2930 fn test_experiment_id_into_u64() {
2931 let id = ExperimentID::from(777777);
2932 let value: u64 = id.into();
2933 assert_eq!(value, 777777);
2934 }
2935
2936 #[test]
2938 fn test_training_session_id_from_u64() {
2939 let id = TrainingSessionID::from(7901234);
2940 assert_eq!(id.value(), 7901234);
2941 }
2942
2943 #[test]
2944 fn test_training_session_id_display() {
2945 let id = TrainingSessionID::from(0xabc123);
2946 assert_eq!(format!("{}", id), "t-abc123");
2947 }
2948
2949 #[test]
2950 fn test_training_session_id_from_str_valid() {
2951 let id = TrainingSessionID::from_str("t-abc123").unwrap();
2952 assert_eq!(id.value(), 0xabc123);
2953 }
2954
2955 #[test]
2956 fn test_training_session_id_try_from_str_valid() {
2957 let id = TrainingSessionID::try_from("t-deadbeef").unwrap();
2958 assert_eq!(id.value(), 0xdeadbeef);
2959 }
2960
2961 #[test]
2962 fn test_training_session_id_try_from_string_valid() {
2963 let id = TrainingSessionID::try_from("t-cafebabe".to_string()).unwrap();
2964 assert_eq!(id.value(), 0xcafebabe);
2965 }
2966
2967 #[test]
2968 fn test_training_session_id_from_str_invalid_prefix() {
2969 let result = TrainingSessionID::from_str("training-123");
2970 assert!(result.is_err());
2971 match result {
2972 Err(Error::InvalidParameters(msg)) => {
2973 assert!(msg.contains("must start with 't-'"));
2974 }
2975 _ => panic!("Expected InvalidParameters error"),
2976 }
2977 }
2978
2979 #[test]
2980 fn test_training_session_id_from_str_invalid_hex() {
2981 let result = TrainingSessionID::from_str("t-qqq");
2982 assert!(result.is_err());
2983 }
2984
2985 #[test]
2986 fn test_training_session_id_into_u64() {
2987 let id = TrainingSessionID::from(123456);
2988 let value: u64 = id.into();
2989 assert_eq!(value, 123456);
2990 }
2991
2992 #[test]
2994 fn test_validation_session_id_from_u64() {
2995 let id = ValidationSessionID::from(3456789);
2996 assert_eq!(id.value(), 3456789);
2997 }
2998
2999 #[test]
3000 fn test_validation_session_id_display() {
3001 let id = ValidationSessionID::from(0x34c985);
3002 assert_eq!(format!("{}", id), "v-34c985");
3003 }
3004
3005 #[test]
3006 fn test_validation_session_id_try_from_str_valid() {
3007 let id = ValidationSessionID::try_from("v-deadbeef").unwrap();
3008 assert_eq!(id.value(), 0xdeadbeef);
3009 }
3010
3011 #[test]
3012 fn test_validation_session_id_try_from_string_valid() {
3013 let id = ValidationSessionID::try_from("v-12345678".to_string()).unwrap();
3014 assert_eq!(id.value(), 0x12345678);
3015 }
3016
3017 #[test]
3018 fn test_validation_session_id_try_from_str_invalid_prefix() {
3019 let result = ValidationSessionID::try_from("validation-123");
3020 assert!(result.is_err());
3021 match result {
3022 Err(Error::InvalidParameters(msg)) => {
3023 assert!(msg.contains("must start with 'v-'"));
3024 }
3025 _ => panic!("Expected InvalidParameters error"),
3026 }
3027 }
3028
3029 #[test]
3030 fn test_validation_session_id_try_from_str_invalid_hex() {
3031 let result = ValidationSessionID::try_from("v-xyz");
3032 assert!(result.is_err());
3033 }
3034
3035 #[test]
3036 fn test_validation_session_id_into_u64() {
3037 let id = ValidationSessionID::from(987654);
3038 let value: u64 = id.into();
3039 assert_eq!(value, 987654);
3040 }
3041
3042 #[test]
3044 fn test_snapshot_id_from_u64() {
3045 let id = SnapshotID::from(111222);
3046 assert_eq!(id.value(), 111222);
3047 }
3048
3049 #[test]
3050 fn test_snapshot_id_display() {
3051 let id = SnapshotID::from(0xaabbcc);
3052 assert_eq!(format!("{}", id), "ss-aabbcc");
3053 }
3054
3055 #[test]
3056 fn test_snapshot_id_try_from_str_valid() {
3057 let id = SnapshotID::try_from("ss-aabbcc").unwrap();
3058 assert_eq!(id.value(), 0xaabbcc);
3059 }
3060
3061 #[test]
3062 fn test_snapshot_id_try_from_str_invalid_prefix() {
3063 let result = SnapshotID::try_from("snapshot-123");
3064 assert!(result.is_err());
3065 match result {
3066 Err(Error::InvalidParameters(msg)) => {
3067 assert!(msg.contains("must start with 'ss-'"));
3068 }
3069 _ => panic!("Expected InvalidParameters error"),
3070 }
3071 }
3072
3073 #[test]
3074 fn test_snapshot_id_try_from_str_invalid_hex() {
3075 let result = SnapshotID::try_from("ss-ggg");
3076 assert!(result.is_err());
3077 }
3078
3079 #[test]
3080 fn test_snapshot_id_into_u64() {
3081 let id = SnapshotID::from(333444);
3082 let value: u64 = id.into();
3083 assert_eq!(value, 333444);
3084 }
3085
3086 #[test]
3088 fn test_task_id_from_u64() {
3089 let id = TaskID::from(555666);
3090 assert_eq!(id.value(), 555666);
3091 }
3092
3093 #[test]
3094 fn test_task_id_display() {
3095 let id = TaskID::from(0x123456);
3096 assert_eq!(format!("{}", id), "task-123456");
3097 }
3098
3099 #[test]
3100 fn test_task_id_from_str_valid() {
3101 let id = TaskID::from_str("task-123456").unwrap();
3102 assert_eq!(id.value(), 0x123456);
3103 }
3104
3105 #[test]
3106 fn test_task_id_try_from_str_valid() {
3107 let id = TaskID::try_from("task-abcdef").unwrap();
3108 assert_eq!(id.value(), 0xabcdef);
3109 }
3110
3111 #[test]
3112 fn test_task_id_try_from_string_valid() {
3113 let id = TaskID::try_from("task-fedcba".to_string()).unwrap();
3114 assert_eq!(id.value(), 0xfedcba);
3115 }
3116
3117 #[test]
3118 fn test_task_id_from_str_invalid_prefix() {
3119 let result = TaskID::from_str("t-123");
3120 assert!(result.is_err());
3121 match result {
3122 Err(Error::InvalidParameters(msg)) => {
3123 assert!(msg.contains("must start with 'task-'"));
3124 }
3125 _ => panic!("Expected InvalidParameters error"),
3126 }
3127 }
3128
3129 #[test]
3130 fn test_task_id_from_str_invalid_hex() {
3131 let result = TaskID::from_str("task-zzz");
3132 assert!(result.is_err());
3133 }
3134
3135 #[test]
3136 fn test_task_id_into_u64() {
3137 let id = TaskID::from(777888);
3138 let value: u64 = id.into();
3139 assert_eq!(value, 777888);
3140 }
3141
3142 #[test]
3144 fn test_dataset_id_from_u64() {
3145 let id = DatasetID::from(1193046);
3146 assert_eq!(id.value(), 1193046);
3147 }
3148
3149 #[test]
3150 fn test_dataset_id_display() {
3151 let id = DatasetID::from(0x123abc);
3152 assert_eq!(format!("{}", id), "ds-123abc");
3153 }
3154
3155 #[test]
3156 fn test_dataset_id_from_str_valid() {
3157 let id = DatasetID::from_str("ds-456def").unwrap();
3158 assert_eq!(id.value(), 0x456def);
3159 }
3160
3161 #[test]
3162 fn test_dataset_id_try_from_str_valid() {
3163 let id = DatasetID::try_from("ds-789abc").unwrap();
3164 assert_eq!(id.value(), 0x789abc);
3165 }
3166
3167 #[test]
3168 fn test_dataset_id_try_from_string_valid() {
3169 let id = DatasetID::try_from("ds-fedcba".to_string()).unwrap();
3170 assert_eq!(id.value(), 0xfedcba);
3171 }
3172
3173 #[test]
3174 fn test_dataset_id_from_str_invalid_prefix() {
3175 let result = DatasetID::from_str("dataset-123");
3176 assert!(result.is_err());
3177 match result {
3178 Err(Error::InvalidParameters(msg)) => {
3179 assert!(msg.contains("must start with 'ds-'"));
3180 }
3181 _ => panic!("Expected InvalidParameters error"),
3182 }
3183 }
3184
3185 #[test]
3186 fn test_dataset_id_from_str_invalid_hex() {
3187 let result = DatasetID::from_str("ds-zzz");
3188 assert!(result.is_err());
3189 }
3190
3191 #[test]
3192 fn test_dataset_id_into_u64() {
3193 let id = DatasetID::from(111111);
3194 let value: u64 = id.into();
3195 assert_eq!(value, 111111);
3196 }
3197
3198 #[test]
3200 fn test_annotation_set_id_from_u64() {
3201 let id = AnnotationSetID::from(222333);
3202 assert_eq!(id.value(), 222333);
3203 }
3204
3205 #[test]
3206 fn test_annotation_set_id_display() {
3207 let id = AnnotationSetID::from(0xabcdef);
3208 assert_eq!(format!("{}", id), "as-abcdef");
3209 }
3210
3211 #[test]
3212 fn test_annotation_set_id_from_str_valid() {
3213 let id = AnnotationSetID::from_str("as-abcdef").unwrap();
3214 assert_eq!(id.value(), 0xabcdef);
3215 }
3216
3217 #[test]
3218 fn test_annotation_set_id_try_from_str_valid() {
3219 let id = AnnotationSetID::try_from("as-123456").unwrap();
3220 assert_eq!(id.value(), 0x123456);
3221 }
3222
3223 #[test]
3224 fn test_annotation_set_id_try_from_string_valid() {
3225 let id = AnnotationSetID::try_from("as-fedcba".to_string()).unwrap();
3226 assert_eq!(id.value(), 0xfedcba);
3227 }
3228
3229 #[test]
3230 fn test_annotation_set_id_from_str_invalid_prefix() {
3231 let result = AnnotationSetID::from_str("annotation-123");
3232 assert!(result.is_err());
3233 match result {
3234 Err(Error::InvalidParameters(msg)) => {
3235 assert!(msg.contains("must start with 'as-'"));
3236 }
3237 _ => panic!("Expected InvalidParameters error"),
3238 }
3239 }
3240
3241 #[test]
3242 fn test_annotation_set_id_from_str_invalid_hex() {
3243 let result = AnnotationSetID::from_str("as-zzz");
3244 assert!(result.is_err());
3245 }
3246
3247 #[test]
3248 fn test_annotation_set_id_into_u64() {
3249 let id = AnnotationSetID::from(444555);
3250 let value: u64 = id.into();
3251 assert_eq!(value, 444555);
3252 }
3253
3254 #[test]
3256 fn test_sample_id_from_u64() {
3257 let id = SampleID::from(666777);
3258 assert_eq!(id.value(), 666777);
3259 }
3260
3261 #[test]
3262 fn test_sample_id_display() {
3263 let id = SampleID::from(0x987654);
3264 assert_eq!(format!("{}", id), "s-987654");
3265 }
3266
3267 #[test]
3268 fn test_sample_id_try_from_str_valid() {
3269 let id = SampleID::try_from("s-987654").unwrap();
3270 assert_eq!(id.value(), 0x987654);
3271 }
3272
3273 #[test]
3274 fn test_sample_id_try_from_str_invalid_prefix() {
3275 let result = SampleID::try_from("sample-123");
3276 assert!(result.is_err());
3277 match result {
3278 Err(Error::InvalidParameters(msg)) => {
3279 assert!(msg.contains("must start with 's-'"));
3280 }
3281 _ => panic!("Expected InvalidParameters error"),
3282 }
3283 }
3284
3285 #[test]
3286 fn test_sample_id_try_from_str_invalid_hex() {
3287 let result = SampleID::try_from("s-zzz");
3288 assert!(result.is_err());
3289 }
3290
3291 #[test]
3292 fn test_sample_id_into_u64() {
3293 let id = SampleID::from(888999);
3294 let value: u64 = id.into();
3295 assert_eq!(value, 888999);
3296 }
3297
3298 #[test]
3300 fn test_app_id_from_u64() {
3301 let id = AppId::from(123123);
3302 assert_eq!(id.value(), 123123);
3303 }
3304
3305 #[test]
3306 fn test_app_id_display() {
3307 let id = AppId::from(0x456789);
3308 assert_eq!(format!("{}", id), "app-456789");
3309 }
3310
3311 #[test]
3312 fn test_app_id_try_from_str_valid() {
3313 let id = AppId::try_from("app-456789").unwrap();
3314 assert_eq!(id.value(), 0x456789);
3315 }
3316
3317 #[test]
3318 fn test_app_id_try_from_str_invalid_prefix() {
3319 let result = AppId::try_from("application-123");
3320 assert!(result.is_err());
3321 match result {
3322 Err(Error::InvalidParameters(msg)) => {
3323 assert!(msg.contains("must start with 'app-'"));
3324 }
3325 _ => panic!("Expected InvalidParameters error"),
3326 }
3327 }
3328
3329 #[test]
3330 fn test_app_id_try_from_str_invalid_hex() {
3331 let result = AppId::try_from("app-zzz");
3332 assert!(result.is_err());
3333 }
3334
3335 #[test]
3336 fn test_app_id_into_u64() {
3337 let id = AppId::from(321321);
3338 let value: u64 = id.into();
3339 assert_eq!(value, 321321);
3340 }
3341
3342 #[test]
3344 fn test_image_id_from_u64() {
3345 let id = ImageId::from(789789);
3346 assert_eq!(id.value(), 789789);
3347 }
3348
3349 #[test]
3350 fn test_image_id_display() {
3351 let id = ImageId::from(0xabcd1234);
3352 assert_eq!(format!("{}", id), "im-abcd1234");
3353 }
3354
3355 #[test]
3356 fn test_image_id_try_from_str_valid() {
3357 let id = ImageId::try_from("im-abcd1234").unwrap();
3358 assert_eq!(id.value(), 0xabcd1234);
3359 }
3360
3361 #[test]
3362 fn test_image_id_try_from_str_invalid_prefix() {
3363 let result = ImageId::try_from("image-123");
3364 assert!(result.is_err());
3365 match result {
3366 Err(Error::InvalidParameters(msg)) => {
3367 assert!(msg.contains("must start with 'im-'"));
3368 }
3369 _ => panic!("Expected InvalidParameters error"),
3370 }
3371 }
3372
3373 #[test]
3374 fn test_image_id_try_from_str_invalid_hex() {
3375 let result = ImageId::try_from("im-zzz");
3376 assert!(result.is_err());
3377 }
3378
3379 #[test]
3380 fn test_image_id_into_u64() {
3381 let id = ImageId::from(987987);
3382 let value: u64 = id.into();
3383 assert_eq!(value, 987987);
3384 }
3385
3386 #[test]
3388 fn test_id_types_equality() {
3389 let id1 = ProjectID::from(12345);
3390 let id2 = ProjectID::from(12345);
3391 let id3 = ProjectID::from(54321);
3392
3393 assert_eq!(id1, id2);
3394 assert_ne!(id1, id3);
3395 }
3396
3397 #[test]
3398 fn test_id_types_hash() {
3399 use std::collections::HashSet;
3400
3401 let mut set = HashSet::new();
3402 set.insert(DatasetID::from(100));
3403 set.insert(DatasetID::from(200));
3404 set.insert(DatasetID::from(100)); assert_eq!(set.len(), 2);
3407 assert!(set.contains(&DatasetID::from(100)));
3408 assert!(set.contains(&DatasetID::from(200)));
3409 }
3410
3411 #[test]
3412 fn test_id_types_copy_clone() {
3413 let id1 = ExperimentID::from(999);
3414 let id2 = id1; let id3 = id1; assert_eq!(id1, id2);
3418 assert_eq!(id1, id3);
3419 }
3420
3421 #[test]
3423 fn test_id_zero_value() {
3424 let id = ProjectID::from(0);
3425 assert_eq!(format!("{}", id), "p-0");
3426 assert_eq!(id.value(), 0);
3427 }
3428
3429 #[test]
3430 fn test_id_max_value() {
3431 let id = ProjectID::from(u64::MAX);
3432 assert_eq!(format!("{}", id), "p-ffffffffffffffff");
3433 assert_eq!(id.value(), u64::MAX);
3434 }
3435
3436 #[test]
3437 fn test_id_round_trip_conversion() {
3438 let original = 0xdeadbeef_u64;
3439 let id = TrainingSessionID::from(original);
3440 let back: u64 = id.into();
3441 assert_eq!(original, back);
3442 }
3443
3444 #[test]
3445 fn test_id_case_insensitive_hex() {
3446 let id1 = DatasetID::from_str("ds-ABCDEF").unwrap();
3448 let id2 = DatasetID::from_str("ds-abcdef").unwrap();
3449 assert_eq!(id1.value(), id2.value());
3450 }
3451
3452 #[test]
3453 fn test_id_with_leading_zeros() {
3454 let id = ProjectID::from_str("p-00001234").unwrap();
3455 assert_eq!(id.value(), 0x1234);
3456 }
3457
3458 #[test]
3460 fn test_parameter_integer() {
3461 let param = Parameter::Integer(42);
3462 match param {
3463 Parameter::Integer(val) => assert_eq!(val, 42),
3464 _ => panic!("Expected Integer variant"),
3465 }
3466 }
3467
3468 #[test]
3469 fn test_parameter_real() {
3470 let param = Parameter::Real(2.5);
3471 match param {
3472 Parameter::Real(val) => assert_eq!(val, 2.5),
3473 _ => panic!("Expected Real variant"),
3474 }
3475 }
3476
3477 #[test]
3478 fn test_parameter_boolean() {
3479 let param = Parameter::Boolean(true);
3480 match param {
3481 Parameter::Boolean(val) => assert!(val),
3482 _ => panic!("Expected Boolean variant"),
3483 }
3484 }
3485
3486 #[test]
3487 fn test_parameter_string() {
3488 let param = Parameter::String("test".to_string());
3489 match param {
3490 Parameter::String(val) => assert_eq!(val, "test"),
3491 _ => panic!("Expected String variant"),
3492 }
3493 }
3494
3495 #[test]
3496 fn test_parameter_array() {
3497 let param = Parameter::Array(vec![
3498 Parameter::Integer(1),
3499 Parameter::Integer(2),
3500 Parameter::Integer(3),
3501 ]);
3502 match param {
3503 Parameter::Array(arr) => assert_eq!(arr.len(), 3),
3504 _ => panic!("Expected Array variant"),
3505 }
3506 }
3507
3508 #[test]
3509 fn test_parameter_object() {
3510 let mut map = HashMap::new();
3511 map.insert("key".to_string(), Parameter::Integer(100));
3512 let param = Parameter::Object(map);
3513 match param {
3514 Parameter::Object(obj) => {
3515 assert_eq!(obj.len(), 1);
3516 assert!(obj.contains_key("key"));
3517 }
3518 _ => panic!("Expected Object variant"),
3519 }
3520 }
3521
3522 #[test]
3523 fn test_parameter_clone() {
3524 let param1 = Parameter::Integer(42);
3525 let param2 = param1.clone();
3526 assert_eq!(param1, param2);
3527 }
3528
3529 #[test]
3530 fn test_parameter_nested() {
3531 let inner_array = Parameter::Array(vec![Parameter::Integer(1), Parameter::Integer(2)]);
3532 let outer_array = Parameter::Array(vec![inner_array.clone(), inner_array]);
3533
3534 match outer_array {
3535 Parameter::Array(arr) => {
3536 assert_eq!(arr.len(), 2);
3537 }
3538 _ => panic!("Expected Array variant"),
3539 }
3540 }
3541
3542 macro_rules! test_typeid_conversions {
3545 ($test_name:ident, $type:ty, $prefix:literal, $wrong_prefix:literal) => {
3546 #[test]
3547 fn $test_name() {
3548 let id = <$type>::from(0xabc123);
3550 assert_eq!(id.value(), 0xabc123);
3551
3552 assert_eq!(format!("{}", id), concat!($prefix, "-abc123"));
3554
3555 let id: $type = concat!($prefix, "-abc123").parse().unwrap();
3557 assert_eq!(id.value(), 0xabc123);
3558
3559 assert!(concat!($wrong_prefix, "-abc").parse::<$type>().is_err());
3561
3562 assert!("abc123".parse::<$type>().is_err());
3564
3565 assert!(concat!($prefix, "-xyz").parse::<$type>().is_err());
3567
3568 let id = <$type>::try_from(concat!($prefix, "-abc123")).unwrap();
3570 assert_eq!(id.value(), 0xabc123);
3571
3572 let id = <$type>::try_from(concat!($prefix, "-abc123").to_string()).unwrap();
3574 assert_eq!(id.value(), 0xabc123);
3575
3576 let id = <$type>::from(0xabc123);
3578 let json = serde_json::to_string(&id).unwrap();
3579 let parsed: $type = serde_json::from_str(&json).unwrap();
3580 assert_eq!(id, parsed);
3581
3582 let id = <$type>::from(0xabc123);
3584 let val: u64 = id.into();
3585 assert_eq!(val, 0xabc123);
3586 }
3587 };
3588 }
3589
3590 test_typeid_conversions!(test_organization_id_conversions, OrganizationID, "org", "p");
3591 test_typeid_conversions!(test_project_id_conversions, ProjectID, "p", "org");
3592 test_typeid_conversions!(test_experiment_id_conversions, ExperimentID, "exp", "p");
3593 test_typeid_conversions!(
3594 test_training_session_id_conversions,
3595 TrainingSessionID,
3596 "t",
3597 "v"
3598 );
3599 test_typeid_conversions!(
3600 test_validation_session_id_conversions,
3601 ValidationSessionID,
3602 "v",
3603 "t"
3604 );
3605 test_typeid_conversions!(test_snapshot_id_conversions, SnapshotID, "ss", "ds");
3606 test_typeid_conversions!(test_task_id_conversions, TaskID, "task", "t");
3607 test_typeid_conversions!(test_dataset_id_conversions, DatasetID, "ds", "ss");
3608 test_typeid_conversions!(
3609 test_annotation_set_id_conversions,
3610 AnnotationSetID,
3611 "as",
3612 "ds"
3613 );
3614 test_typeid_conversions!(test_sample_id_conversions, SampleID, "s", "p");
3615 test_typeid_conversions!(test_app_id_conversions, AppId, "app", "p");
3616 test_typeid_conversions!(test_image_id_conversions, ImageId, "im", "se");
3617 test_typeid_conversions!(test_sequence_id_conversions, SequenceId, "se", "im");
3618
3619 #[test]
3622 fn test_version_tag_deserialize_full() {
3623 let json = r#"{
3624 "id": 456, "dataset_id": 1715004, "name": "training-v1.0",
3625 "serial": 42, "description": "Ready for production",
3626 "created_by": "user@example.com", "created_at": "2025-01-15T10:30:00Z",
3627 "image_count": 50000, "annotation_counts": {"box": 150000, "seg": 20000},
3628 "sensor_counts": {"lidar": 25000}, "label_count": 15,
3629 "annotation_set_count": 3, "snapshot_id": 789
3630 }"#;
3631 let tag: VersionTag = serde_json::from_str(json).unwrap();
3632 assert_eq!(tag.name(), "training-v1.0");
3633 assert_eq!(tag.serial(), 42);
3634 assert_eq!(tag.image_count(), 50000);
3635 assert_eq!(tag.annotation_counts().get("box"), Some(&150000));
3636 assert_eq!(tag.snapshot_id(), Some(789));
3637 }
3638
3639 #[test]
3640 fn test_version_tag_deserialize_omitempty() {
3641 let json = r#"{
3643 "id": 1, "dataset_id": 2, "name": "v1.0", "serial": 5,
3644 "description": "", "created_by": "user",
3645 "created_at": "2025-01-01T00:00:00Z"
3646 }"#;
3647 let tag: VersionTag = serde_json::from_str(json).unwrap();
3648 assert_eq!(tag.snapshot_id(), None);
3649 assert_eq!(tag.image_count(), 0);
3650 assert!(tag.annotation_counts().is_empty());
3651 }
3652
3653 #[test]
3654 fn test_changelog_entry_deserialize_omitempty() {
3655 let json = r#"{
3657 "id": 1, "dataset_id": 2, "serial": 3, "entity_type": "image",
3658 "operation": "bulk_create", "change_data": {"count": 5},
3659 "username": "user", "organization_id": 1,
3660 "created_at": "2025-01-01T00:00:00Z", "message": ""
3661 }"#;
3662 let entry: ChangelogEntry = serde_json::from_str(json).unwrap();
3663 assert!(entry.entity_id().is_none());
3664 assert!(entry.s3_version_ids().is_empty());
3665 assert_eq!(entry.entity_type(), "image");
3666 assert_eq!(entry.operation(), "bulk_create");
3667 }
3668
3669 #[test]
3670 fn test_changelog_response_deserialize() {
3671 let json = r#"{
3672 "entries": [], "count": 0, "continue_token": ""
3673 }"#;
3674 let resp: ChangelogResponse = serde_json::from_str(json).unwrap();
3675 assert!(resp.entries.is_empty());
3676 assert_eq!(resp.count, 0);
3677 assert!(resp.continue_token.is_empty());
3678 assert!(resp.from_serial.is_none());
3679 }
3680
3681 #[test]
3682 fn test_version_current_no_latest_tag() {
3683 let json = r#"{
3685 "dataset_id": 100, "current_serial": 5, "tags": []
3686 }"#;
3687 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3688 assert!(resp.latest_tag.is_none());
3689 assert!(resp.tags.is_empty());
3690 assert_eq!(resp.current_serial, 5);
3691 }
3692
3693 #[test]
3694 fn test_version_current_with_latest_tag() {
3695 let json = r#"{
3696 "dataset_id": 100, "current_serial": 42,
3697 "latest_tag": {
3698 "id": 1, "dataset_id": 100, "name": "v1.0", "serial": 42,
3699 "description": "test", "created_by": "user",
3700 "created_at": "2025-01-01T00:00:00Z",
3701 "image_count": 10, "label_count": 2, "annotation_set_count": 1
3702 },
3703 "tags": []
3704 }"#;
3705 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3706 assert!(resp.latest_tag.is_some());
3707 assert_eq!(resp.latest_tag.unwrap().name(), "v1.0");
3708 }
3709
3710 #[test]
3711 fn test_version_tag_is_current_field() {
3712 let json = r#"{
3713 "id": 1, "dataset_id": 5, "name": "v1.0", "serial": 10,
3714 "created_by": "alice", "created_at": "2026-01-01T00:00:00Z",
3715 "is_current": true
3716 }"#;
3717 let tag: VersionTag = serde_json::from_str(json).unwrap();
3718 assert!(tag.is_current());
3719 }
3720
3721 #[test]
3722 fn test_version_tag_is_current_false() {
3723 let json = r#"{
3724 "id": 1, "dataset_id": 5, "name": "v1.0", "serial": 10,
3725 "created_by": "alice", "created_at": "2026-01-01T00:00:00Z",
3726 "is_current": false
3727 }"#;
3728 let tag: VersionTag = serde_json::from_str(json).unwrap();
3729 assert!(!tag.is_current());
3730 }
3731
3732 #[test]
3733 fn test_dataset_summary_deserialize() {
3734 let json = r#"{
3735 "dataset_id": 100, "current_serial": 10,
3736 "image_count": 5000, "annotation_counts": {"box": 10000},
3737 "sensor_counts": {}, "label_count": 8,
3738 "annotation_set_count": 2, "last_updated": "2025-06-01T12:00:00Z"
3739 }"#;
3740 let summary: DatasetSummary = serde_json::from_str(json).unwrap();
3741 assert_eq!(summary.image_count(), 5000);
3742 assert_eq!(summary.label_count(), 8);
3743 assert_eq!(summary.annotation_counts().get("box"), Some(&10000));
3744 }
3745
3746 #[test]
3747 fn test_restore_result_deserialize() {
3748 let json = r#"{
3749 "success": true, "new_serial": 45,
3750 "restored_from": {"tag": "v1.0", "serial": 42},
3751 "restored_counts": {"images": 5000, "labels": 15, "annotation_sets": 3},
3752 "message": "Dataset restored to tag v1.0"
3753 }"#;
3754 let result: RestoreResult = serde_json::from_str(json).unwrap();
3755 assert!(result.success);
3756 assert_eq!(result.new_serial, 45);
3757 assert_eq!(result.restored_from.tag, "v1.0");
3758 assert_eq!(result.restored_from.serial, 42);
3759 assert_eq!(result.restored_counts.images, 5000);
3760 }
3761
3762 #[test]
3763 fn test_sample_delete_params_serializes_all_fields() {
3764 let params = SampleDeleteParams {
3770 dataset_id: 42,
3771 image_ids: vec![1, 2, 3],
3772 sequence_ids: Vec::new(),
3773 delete_all: false,
3774 };
3775 let value = serde_json::to_value(¶ms).unwrap();
3776 let obj = value.as_object().unwrap();
3777 assert_eq!(obj.len(), 4);
3778 assert_eq!(obj["dataset_id"], serde_json::json!(42));
3779 assert_eq!(obj["image_ids"], serde_json::json!([1, 2, 3]));
3780 assert_eq!(obj["sequence_ids"], serde_json::json!([]));
3781 assert_eq!(obj["delete_all"], serde_json::json!(false));
3782 }
3783}
3784
3785#[cfg(test)]
3786mod tests_task_data_list {
3787 use super::*;
3788
3789 #[test]
3790 fn task_data_list_deserializes_from_server_shape() {
3791 let json = r#"{
3792 "server": "test.edgefirst.studio",
3793 "organization_uid": "org-abc123",
3794 "traces": ["trace/imx95.json"],
3795 "data": {
3796 "predictions": ["predictions.parquet"],
3797 "trace": ["imx95.json"]
3798 }
3799 }"#;
3800 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
3801 assert_eq!(parsed.server, "test.edgefirst.studio");
3802 assert_eq!(parsed.organization_uid, "org-abc123");
3803 assert_eq!(parsed.traces, vec!["trace/imx95.json"]);
3804 assert_eq!(
3805 parsed.data.get("predictions").unwrap(),
3806 &vec!["predictions.parquet".to_string()]
3807 );
3808 }
3809}
3810
3811#[cfg(test)]
3812mod tests_upload_data {
3813 #[test]
3817 fn folder_empty_string_is_normalised() {
3818 let folder: Option<&str> = Some("");
3819 assert!(folder.filter(|s| !s.is_empty()).is_none());
3820
3821 let folder_real: Option<&str> = Some("predictions");
3822 assert!(folder_real.filter(|s| !s.is_empty()).is_some());
3823 }
3824}
3825
3826#[cfg(test)]
3827mod tests_job_struct {
3828 use super::*;
3829
3830 #[test]
3831 fn job_deserializes_with_all_fields() {
3832 let json = r#"{
3833 "code": "edgefirst-validator:2.9.5",
3834 "title": "EdgeFirst Validator",
3835 "job_name": "smoke-test",
3836 "job_id": "aws-batch-abc",
3837 "state": "RUNNING",
3838 "launch": "2026-05-14T15:00:00Z",
3839 "task_id": 6789
3840 }"#;
3841 let job: Job = serde_json::from_str(json).unwrap();
3842 assert_eq!(job.code, "edgefirst-validator:2.9.5");
3843 assert_eq!(job.title, "EdgeFirst Validator");
3844 assert_eq!(job.job_name, "smoke-test");
3845 assert_eq!(job.job_id, "aws-batch-abc");
3846 assert_eq!(job.state, "RUNNING");
3847 assert!(job.launch.is_some());
3848 assert_eq!(job.task_id, 6789);
3849 }
3850
3851 #[test]
3852 fn job_tolerates_missing_optional_fields() {
3853 let json = r#"{ "task_id": 42 }"#;
3857 let job: Job = serde_json::from_str(json).unwrap();
3858 assert_eq!(job.task_id, 42);
3859 assert!(job.code.is_empty());
3860 assert!(job.title.is_empty());
3861 assert!(job.job_name.is_empty());
3862 assert!(job.job_id.is_empty());
3863 assert!(job.state.is_empty());
3864 assert!(job.launch.is_none());
3865 }
3866
3867 #[test]
3868 fn job_task_id_accessor_saturates_negative_to_zero() {
3869 let job = Job {
3874 code: String::new(),
3875 title: String::new(),
3876 job_name: String::new(),
3877 job_id: String::new(),
3878 state: String::new(),
3879 launch: None,
3880 task_id: -1,
3881 };
3882 assert_eq!(job.task_id().value(), 0);
3883 }
3884
3885 #[test]
3886 fn job_task_id_accessor_passes_through_positive_values() {
3887 let job = Job {
3888 code: String::new(),
3889 title: String::new(),
3890 job_name: String::new(),
3891 job_id: String::new(),
3892 state: String::new(),
3893 launch: None,
3894 task_id: 12345,
3895 };
3896 assert_eq!(job.task_id().value(), 12345);
3897 }
3898
3899 #[test]
3900 fn job_ignores_unknown_fields() {
3901 let json = r#"{
3905 "code": "x",
3906 "task_id": 1,
3907 "docker_task": { "image": "x" },
3908 "aws_region": "us-east-1",
3909 "tags": ["a", "b"]
3910 }"#;
3911 let job: Job = serde_json::from_str(json).unwrap();
3912 assert_eq!(job.task_id, 1);
3913 }
3914}
3915
3916#[cfg(test)]
3917mod tests_task_info_schema_tolerance {
3918 use super::*;
3919
3920 #[test]
3925 fn task_info_accepts_task_description_field() {
3926 let json = r#"{
3928 "id": 6699,
3929 "type": "edgefirst-validator:2.9.5",
3930 "task_description": "Profiler run for IMX95",
3931 "status": "running"
3932 }"#;
3933 let info: TaskInfo = serde_json::from_str(json).unwrap();
3934 assert_eq!(info.description(), "Profiler run for IMX95");
3935 }
3936
3937 #[test]
3938 fn task_info_accepts_legacy_description_field() {
3939 let json = r#"{
3941 "id": 6699,
3942 "type": "edgefirst-validator:2.9.5",
3943 "description": "Legacy description"
3944 }"#;
3945 let info: TaskInfo = serde_json::from_str(json).unwrap();
3946 assert_eq!(info.description(), "Legacy description");
3947 }
3948
3949 #[test]
3950 fn task_info_tolerates_missing_description() {
3951 let json = r#"{
3953 "id": 6699,
3954 "type": "x"
3955 }"#;
3956 let info: TaskInfo = serde_json::from_str(json).unwrap();
3957 assert!(info.description().is_empty());
3958 }
3959
3960 #[test]
3961 fn task_info_tolerates_missing_dates_via_default() {
3962 let json = r#"{
3964 "id": 6699,
3965 "type": "x"
3966 }"#;
3967 let info: TaskInfo = serde_json::from_str(json).unwrap();
3968 assert_eq!(info.id().value(), 6699);
3970 }
3971
3972 #[test]
3973 fn task_info_status_accessor_returns_option() {
3974 let json = r#"{
3975 "id": 1,
3976 "type": "x"
3977 }"#;
3978 let info: TaskInfo = serde_json::from_str(json).unwrap();
3979 assert!(info.status().is_none());
3980 }
3981
3982 #[test]
3983 fn task_info_stages_returns_empty_map_when_unset() {
3984 let json = r#"{
3985 "id": 1,
3986 "type": "x"
3987 }"#;
3988 let info: TaskInfo = serde_json::from_str(json).unwrap();
3989 let stages = info.stages();
3990 assert!(stages.is_empty());
3991 }
3992}
3993
3994#[cfg(test)]
3995mod tests_stage_struct {
3996 use super::*;
3997
3998 #[test]
3999 fn stage_new_sets_only_supplied_fields() {
4000 let stage = Stage::new(
4001 None,
4002 "download".into(),
4003 Some("running".into()),
4004 Some("fetching".into()),
4005 42,
4006 );
4007 assert!(stage.task_id().is_none());
4008 assert_eq!(stage.stage(), "download");
4009 assert_eq!(stage.status().as_deref(), Some("running"));
4010 assert_eq!(stage.message().as_deref(), Some("fetching"));
4011 assert_eq!(stage.percentage(), 42);
4012 assert!(stage.description().is_none());
4014 }
4015
4016 #[test]
4017 fn stage_serializes_without_optional_none_fields() {
4018 let stage = Stage::new(None, "init".into(), None, None, 0);
4020 let json = serde_json::to_value(&stage).unwrap();
4021 assert!(json.get("status").is_none(), "got: {json}");
4022 assert!(json.get("message").is_none(), "got: {json}");
4023 assert!(json.get("docker_task_id").is_none(), "got: {json}");
4024 assert_eq!(json["stage"], "init");
4026 assert_eq!(json["percentage"], 0);
4027 }
4028
4029 #[test]
4030 fn stage_serializes_task_id_when_present() {
4031 let task_id = TaskID::from(0xdeadu64);
4032 let stage = Stage::new(Some(task_id), "x".into(), None, None, 0);
4033 let json = serde_json::to_value(&stage).unwrap();
4034 assert!(json.get("docker_task_id").is_some());
4037 }
4038
4039 #[test]
4040 fn stage_round_trips_through_json() {
4041 let stage = Stage::new(
4042 None,
4043 "train".into(),
4044 Some("done".into()),
4045 Some("epoch 100".into()),
4046 100,
4047 );
4048 let s = serde_json::to_string(&stage).unwrap();
4049 let back: Stage = serde_json::from_str(&s).unwrap();
4050 assert_eq!(back.stage(), "train");
4051 assert_eq!(back.status().as_deref(), Some("done"));
4052 assert_eq!(back.message().as_deref(), Some("epoch 100"));
4053 assert_eq!(back.percentage(), 100);
4054 }
4055}
4056
4057#[cfg(test)]
4058mod tests_task_data_list_extra {
4059 use super::*;
4060
4061 #[test]
4062 fn task_data_list_with_empty_data_map() {
4063 let json = r#"{
4064 "server": "studio",
4065 "organization_uid": "org-1",
4066 "traces": [],
4067 "data": {}
4068 }"#;
4069 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4070 assert!(parsed.traces.is_empty());
4071 assert!(parsed.data.is_empty());
4072 }
4073
4074 #[test]
4075 fn task_data_list_multiple_folders() {
4076 let json = r#"{
4077 "server": "studio",
4078 "organization_uid": "org-1",
4079 "traces": ["t1", "t2"],
4080 "data": {
4081 "predictions": ["a.parquet", "b.parquet"],
4082 "metrics": ["loss.json"]
4083 }
4084 }"#;
4085 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4086 assert_eq!(parsed.traces.len(), 2);
4087 assert_eq!(parsed.data.len(), 2);
4088 assert_eq!(parsed.data["predictions"].len(), 2);
4089 }
4090}
4091
4092#[cfg(test)]
4093mod tests_artifact_struct {
4094 use super::*;
4095
4096 #[test]
4097 fn artifact_accessors_return_strs() {
4098 let json = r#"{ "name": "best.onnx", "modelType": "yolo" }"#;
4101 let a: Artifact = serde_json::from_str(json).unwrap();
4102 assert_eq!(a.name(), "best.onnx");
4103 assert_eq!(a.model_type(), "yolo");
4104 }
4105}
4106
4107#[cfg(test)]
4108mod tests_task_status_serialize {
4109 use super::*;
4110
4111 #[test]
4112 fn task_status_uses_docker_task_id_wire_field() {
4113 let s = TaskStatus {
4114 task_id: TaskID::from(0x1a2bu64),
4115 status: "training".into(),
4116 };
4117 let json = serde_json::to_value(&s).unwrap();
4118 assert!(json.get("docker_task_id").is_some(), "got: {json}");
4120 assert_eq!(json["status"], "training");
4121 }
4122}
4123
4124#[cfg(test)]
4125mod tests_task_stages_serialize {
4126 use super::*;
4127
4128 #[test]
4129 fn task_stages_omits_empty_vec() {
4130 let stages = TaskStages {
4131 task_id: TaskID::from(1u64),
4132 stages: Vec::new(),
4133 };
4134 let json = serde_json::to_value(&stages).unwrap();
4135 assert!(json.get("stages").is_none(), "got: {json}");
4137 }
4138
4139 #[test]
4140 fn task_stages_serializes_non_empty_vec() {
4141 let stages = TaskStages {
4142 task_id: TaskID::from(1u64),
4143 stages: vec![std::collections::HashMap::from([(
4144 "stage".to_string(),
4145 "download".to_string(),
4146 )])],
4147 };
4148 let json = serde_json::to_value(&stages).unwrap();
4149 assert_eq!(json["stages"][0]["stage"], "download");
4150 }
4151}