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, Default)]
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")]
203 credits: f64,
204}
205
206impl Display for Organization {
207 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
208 write!(f, "{}", self.name())
209 }
210}
211
212impl Organization {
213 pub fn id(&self) -> OrganizationID {
214 self.id
215 }
216
217 pub fn name(&self) -> &str {
218 &self.name
219 }
220
221 pub fn credits(&self) -> f64 {
222 self.credits
223 }
224}
225
226#[derive(Deserialize, Clone, Debug)]
232pub struct UsageSummary {
233 #[serde(default)]
234 credits: f64,
235 #[serde(default)]
236 funds: f64,
237 #[serde(default, rename = "total_funds_and_credits")]
238 total: f64,
239}
240
241impl UsageSummary {
242 pub fn credits(&self) -> f64 {
243 self.credits
244 }
245
246 pub fn funds(&self) -> f64 {
247 self.funds
248 }
249
250 pub fn total(&self) -> f64 {
251 self.total
252 }
253}
254
255typeid!(
256 ProjectID,
277 "p"
278);
279
280typeid!(
281 ExperimentID,
302 "exp"
303);
304
305typeid!(
306 TrainingSessionID,
327 "t"
328);
329
330typeid!(
331 ValidationSessionID,
351 "v"
352);
353
354typeid!(
355 SnapshotID,
371 "ss"
372);
373
374typeid!(
375 TaskID,
406 "task"
407);
408
409typeid!(
410 BackgroundTaskID,
439 "bt"
440);
441
442impl From<BackgroundTaskID> for TaskID {
448 fn from(id: BackgroundTaskID) -> Self {
449 TaskID::from(id.value())
450 }
451}
452
453impl From<TaskID> for BackgroundTaskID {
454 fn from(id: TaskID) -> Self {
455 BackgroundTaskID::from(id.value())
456 }
457}
458
459typeid!(
460 DatasetID,
481 "ds"
482);
483
484typeid!(
485 AnnotationSetID,
501 "as"
502);
503
504typeid!(
505 SampleID,
521 "s"
522);
523
524typeid!(
525 AppId,
531 "app"
532);
533
534typeid!(
535 ImageId,
541 "im"
542);
543
544typeid!(
545 SequenceId,
551 "se"
552);
553
554#[derive(Deserialize, Clone, Debug)]
558pub struct Project {
559 id: ProjectID,
560 name: String,
561 description: String,
562}
563
564impl Display for Project {
565 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
566 write!(f, "{} {}", self.id(), self.name())
567 }
568}
569
570impl Project {
571 pub fn id(&self) -> ProjectID {
572 self.id
573 }
574
575 pub fn name(&self) -> &str {
576 &self.name
577 }
578
579 pub fn description(&self) -> &str {
580 &self.description
581 }
582
583 pub async fn datasets(
584 &self,
585 client: &client::Client,
586 name: Option<&str>,
587 ) -> Result<Vec<Dataset>, Error> {
588 client.datasets(self.id, name).await
589 }
590
591 pub async fn experiments(
592 &self,
593 client: &client::Client,
594 name: Option<&str>,
595 ) -> Result<Vec<Experiment>, Error> {
596 client.experiments(self.id, name).await
597 }
598}
599
600#[derive(Deserialize, Debug)]
601pub struct SamplesCountResult {
602 pub total: u64,
603}
604
605#[derive(Serialize, Clone, Debug)]
606pub struct SamplesListParams {
607 pub dataset_id: DatasetID,
608 #[serde(skip_serializing_if = "Option::is_none")]
609 pub annotation_set_id: Option<AnnotationSetID>,
610 #[serde(skip_serializing_if = "Option::is_none")]
611 pub continue_token: Option<String>,
612 #[serde(skip_serializing_if = "Vec::is_empty")]
613 pub types: Vec<String>,
614 #[serde(skip_serializing_if = "Vec::is_empty")]
615 pub group_names: Vec<String>,
616 #[serde(skip_serializing_if = "Option::is_none")]
617 pub tag: Option<String>,
618 #[serde(skip_serializing_if = "Option::is_none")]
622 pub limit: Option<u32>,
623}
624
625#[derive(Deserialize, Debug)]
626pub struct SamplesListResult {
627 pub samples: Vec<Sample>,
628 pub continue_token: Option<String>,
629}
630
631#[derive(Serialize, Clone, Debug)]
633pub struct SampleDimensionUpdate {
634 pub id: SampleID,
635 pub width: u32,
636 pub height: u32,
637}
638
639#[derive(Serialize, Clone, Debug)]
641pub struct SamplesUpdateDimensionsParams {
642 pub dataset_id: DatasetID,
643 pub samples: Vec<SampleDimensionUpdate>,
644}
645
646#[derive(Deserialize, Debug)]
648pub struct SamplesUpdateDimensionsResult {
649 pub updated: u64,
650}
651
652#[derive(Serialize, Clone, Debug)]
657pub struct SamplesPopulateParams {
658 pub dataset_id: DatasetID,
659 #[serde(skip_serializing_if = "Option::is_none")]
660 pub annotation_set_id: Option<AnnotationSetID>,
661 #[serde(skip_serializing_if = "Option::is_none")]
662 pub presigned_urls: Option<bool>,
663 pub samples: Vec<Sample>,
664}
665
666#[derive(Deserialize, Debug, Clone)]
672pub struct SamplesPopulateResult {
673 pub uuid: String,
675 pub urls: Vec<PresignedUrl>,
677}
678
679#[derive(Deserialize, Debug, Clone)]
681pub struct PresignedUrl {
682 pub filename: String,
684 pub key: String,
686 pub url: String,
688}
689
690#[derive(Serialize, Clone, Debug)]
703pub struct ServerAnnotation {
704 #[serde(skip_serializing_if = "Option::is_none")]
706 pub label_id: Option<u64>,
707 #[serde(skip_serializing_if = "Option::is_none")]
709 pub label_index: Option<u64>,
710 #[serde(skip_serializing_if = "Option::is_none")]
712 pub label_name: Option<String>,
713 #[serde(rename = "type")]
715 pub annotation_type: String,
716 pub x: f64,
718 pub y: f64,
720 pub w: f64,
722 pub h: f64,
724 pub score: f64,
726 #[serde(skip_serializing_if = "String::is_empty")]
728 pub polygon: String,
729 pub image_id: u64,
731 pub annotation_set_id: u64,
733 #[serde(skip_serializing_if = "Option::is_none")]
735 pub object_reference: Option<String>,
736}
737
738#[derive(Serialize, Debug)]
740pub struct AnnotationAddBulkParams {
741 pub annotation_set_id: u64,
742 pub annotations: Vec<ServerAnnotation>,
743}
744
745#[derive(Serialize, Debug)]
747pub struct AnnotationBulkDeleteParams {
748 pub annotation_set_id: u64,
749 pub annotation_types: Vec<String>,
750 #[serde(skip_serializing_if = "Vec::is_empty")]
752 pub image_ids: Vec<u64>,
753 #[serde(skip_serializing_if = "Option::is_none")]
755 pub delete_all: Option<bool>,
756}
757
758#[derive(Serialize, Debug)]
766pub struct SampleDeleteParams {
767 pub dataset_id: u64,
768 pub image_ids: Vec<u64>,
769 pub sequence_ids: Vec<i64>,
770 pub delete_all: bool,
771}
772
773#[derive(Deserialize)]
774pub struct Snapshot {
775 id: SnapshotID,
776 description: String,
777 status: String,
778 path: String,
779 #[serde(rename = "date")]
780 created: DateTime<Utc>,
781}
782
783impl Display for Snapshot {
784 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
785 write!(f, "{} {}", self.id, self.description)
786 }
787}
788
789impl Snapshot {
790 pub fn id(&self) -> SnapshotID {
791 self.id
792 }
793
794 pub fn description(&self) -> &str {
795 &self.description
796 }
797
798 pub fn status(&self) -> &str {
799 &self.status
800 }
801
802 pub fn path(&self) -> &str {
803 &self.path
804 }
805
806 pub fn created(&self) -> &DateTime<Utc> {
807 &self.created
808 }
809}
810
811#[derive(Serialize, Debug)]
812pub struct SnapshotRestore {
813 pub project_id: ProjectID,
814 pub snapshot_id: SnapshotID,
815 pub fps: u64,
816 #[serde(rename = "enabled_topics", skip_serializing_if = "Vec::is_empty")]
817 pub topics: Vec<String>,
818 #[serde(rename = "label_names", skip_serializing_if = "Vec::is_empty")]
819 pub autolabel: Vec<String>,
820 #[serde(rename = "depth_gen")]
821 pub autodepth: bool,
822 pub agtg_pipeline: bool,
823 #[serde(skip_serializing_if = "Option::is_none")]
824 pub dataset_name: Option<String>,
825 #[serde(skip_serializing_if = "Option::is_none")]
826 pub dataset_description: Option<String>,
827}
828
829#[derive(Deserialize, Debug)]
830pub struct SnapshotRestoreResult {
831 pub id: SnapshotID,
832 pub description: String,
833 pub dataset_name: String,
834 pub dataset_id: DatasetID,
835 pub annotation_set_id: AnnotationSetID,
836 #[serde(default)]
837 pub task_id: Option<TaskID>,
838 #[serde(default)]
842 pub date: Option<DateTime<Utc>>,
843}
844
845#[derive(Serialize, Debug)]
850pub struct SnapshotCreateFromDataset {
851 pub description: String,
853 pub dataset_id: DatasetID,
855 pub annotation_set_id: AnnotationSetID,
857}
858
859#[derive(Deserialize, Debug)]
863pub struct SnapshotFromDatasetResult {
864 #[serde(alias = "snapshot_id")]
866 pub id: SnapshotID,
867 #[serde(default)]
869 pub task_id: Option<TaskID>,
870}
871
872#[derive(Deserialize)]
873pub struct Experiment {
874 id: ExperimentID,
875 project_id: ProjectID,
876 name: String,
877 description: String,
878}
879
880impl Display for Experiment {
881 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
882 write!(f, "{} {}", self.id, self.name)
883 }
884}
885
886impl Experiment {
887 pub fn id(&self) -> ExperimentID {
888 self.id
889 }
890
891 pub fn project_id(&self) -> ProjectID {
892 self.project_id
893 }
894
895 pub fn name(&self) -> &str {
896 &self.name
897 }
898
899 pub fn description(&self) -> &str {
900 &self.description
901 }
902
903 pub async fn project(&self, client: &client::Client) -> Result<Project, Error> {
904 client.project(self.project_id).await
905 }
906
907 pub async fn training_sessions(
908 &self,
909 client: &client::Client,
910 name: Option<&str>,
911 ) -> Result<Vec<TrainingSession>, Error> {
912 client.training_sessions(self.id, name).await
913 }
914}
915
916#[derive(Serialize, Debug)]
917pub struct PublishMetrics {
918 #[serde(rename = "trainer_session_id", skip_serializing_if = "Option::is_none")]
919 pub trainer_session_id: Option<TrainingSessionID>,
920 #[serde(
921 rename = "validate_session_id",
922 skip_serializing_if = "Option::is_none"
923 )]
924 pub validate_session_id: Option<ValidationSessionID>,
925 pub metrics: HashMap<String, Parameter>,
926}
927
928#[derive(Deserialize)]
929struct TrainingSessionParams {
930 #[serde(default)]
931 model_params: HashMap<String, Parameter>,
932 #[serde(default)]
933 dataset_params: DatasetParams,
934}
935
936#[derive(Deserialize)]
937pub struct TrainingSession {
938 id: TrainingSessionID,
939 #[serde(rename = "trainer_id")]
940 experiment_id: ExperimentID,
941 model: String,
942 name: String,
943 description: String,
944 params: TrainingSessionParams,
945 #[serde(rename = "docker_task")]
946 task: Task,
947}
948
949impl Display for TrainingSession {
950 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
951 write!(f, "{} {}", self.id, self.name())
952 }
953}
954
955impl TrainingSession {
956 pub fn id(&self) -> TrainingSessionID {
957 self.id
958 }
959
960 pub fn name(&self) -> &str {
961 &self.name
962 }
963
964 pub fn description(&self) -> &str {
965 &self.description
966 }
967
968 pub fn model(&self) -> &str {
969 &self.model
970 }
971
972 pub fn experiment_id(&self) -> ExperimentID {
973 self.experiment_id
974 }
975
976 pub fn task(&self) -> Task {
977 self.task.clone()
978 }
979
980 pub fn model_params(&self) -> &HashMap<String, Parameter> {
981 &self.params.model_params
982 }
983
984 pub fn dataset_params(&self) -> &DatasetParams {
985 &self.params.dataset_params
986 }
987
988 pub fn train_group(&self) -> &str {
989 &self.params.dataset_params.train_group
990 }
991
992 pub fn val_group(&self) -> &str {
993 &self.params.dataset_params.val_group
994 }
995
996 pub async fn experiment(&self, client: &client::Client) -> Result<Experiment, Error> {
997 client.experiment(self.experiment_id).await
998 }
999
1000 pub async fn dataset(&self, client: &client::Client) -> Result<Dataset, Error> {
1001 if self.params.dataset_params.dataset_id.value() == 0 {
1002 return Err(Error::InvalidParameters(
1003 "training session has no dataset configured".into(),
1004 ));
1005 }
1006 client.dataset(self.params.dataset_params.dataset_id).await
1007 }
1008
1009 pub async fn annotation_set(&self, client: &client::Client) -> Result<AnnotationSet, Error> {
1010 if self.params.dataset_params.annotation_set_id.value() == 0 {
1011 return Err(Error::InvalidParameters(
1012 "training session has no annotation set configured".into(),
1013 ));
1014 }
1015 client
1016 .annotation_set(self.params.dataset_params.annotation_set_id)
1017 .await
1018 }
1019
1020 pub async fn artifacts(&self, client: &client::Client) -> Result<Vec<Artifact>, Error> {
1021 client.artifacts(self.id).await
1022 }
1023
1024 pub async fn metrics(
1025 &self,
1026 client: &client::Client,
1027 ) -> Result<HashMap<String, Parameter>, Error> {
1028 #[derive(Deserialize)]
1029 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1030 enum Response {
1031 Empty {},
1032 Map(HashMap<String, Parameter>),
1033 String(String),
1034 }
1035
1036 let params = HashMap::from([("trainer_session_id", self.id().value())]);
1037 let resp: Response = client
1038 .rpc("trainer.session.metrics".to_owned(), Some(params))
1039 .await?;
1040
1041 Ok(match resp {
1042 Response::String(metrics) => serde_json::from_str(&metrics)?,
1043 Response::Map(metrics) => metrics,
1044 Response::Empty {} => HashMap::new(),
1045 })
1046 }
1047
1048 pub async fn set_metrics(
1049 &self,
1050 client: &client::Client,
1051 metrics: HashMap<String, Parameter>,
1052 ) -> Result<(), Error> {
1053 let metrics = PublishMetrics {
1054 trainer_session_id: Some(self.id()),
1055 validate_session_id: None,
1056 metrics,
1057 };
1058
1059 let _: String = client
1060 .rpc("trainer.session.metrics".to_owned(), Some(metrics))
1061 .await?;
1062
1063 Ok(())
1064 }
1065
1066 pub async fn download_artifact(
1068 &self,
1069 client: &client::Client,
1070 filename: &str,
1071 ) -> Result<Vec<u8>, Error> {
1072 client
1073 .fetch(&format!(
1074 "download_model?training_session_id={}&file={}",
1075 self.id().value(),
1076 filename
1077 ))
1078 .await
1079 }
1080
1081 pub async fn upload_artifact(
1085 &self,
1086 client: &client::Client,
1087 filename: &str,
1088 path: PathBuf,
1089 ) -> Result<(), Error> {
1090 self.upload(client, &[(format!("artifacts/{}", filename), path)])
1091 .await
1092 }
1093
1094 pub async fn download_checkpoint(
1096 &self,
1097 client: &client::Client,
1098 filename: &str,
1099 ) -> Result<Vec<u8>, Error> {
1100 client
1101 .fetch(&format!(
1102 "download_checkpoint?folder=checkpoints&training_session_id={}&file={}",
1103 self.id().value(),
1104 filename
1105 ))
1106 .await
1107 }
1108
1109 pub async fn upload_checkpoint(
1113 &self,
1114 client: &client::Client,
1115 filename: &str,
1116 path: PathBuf,
1117 ) -> Result<(), Error> {
1118 self.upload(client, &[(format!("checkpoints/{}", filename), path)])
1119 .await
1120 }
1121
1122 pub async fn download(&self, client: &client::Client, filename: &str) -> Result<String, Error> {
1126 #[derive(Serialize)]
1127 struct DownloadRequest {
1128 session_id: TrainingSessionID,
1129 file_path: String,
1130 }
1131
1132 let params = DownloadRequest {
1133 session_id: self.id(),
1134 file_path: filename.to_string(),
1135 };
1136
1137 client
1138 .rpc("trainer.download.file".to_owned(), Some(params))
1139 .await
1140 }
1141
1142 pub async fn upload(
1143 &self,
1144 client: &client::Client,
1145 files: &[(String, PathBuf)],
1146 ) -> Result<(), Error> {
1147 let mut parts = Form::new().part(
1148 "params",
1149 Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1150 );
1151
1152 for (name, path) in files {
1153 let file_part = Part::file(path).await?.file_name(name.to_owned());
1154 parts = parts.part("file", file_part);
1155 }
1156
1157 let result = client.post_multipart("trainer.upload.files", parts).await?;
1158 trace!("TrainingSession::upload: {:?}", result);
1159 Ok(())
1160 }
1161}
1162
1163#[derive(Deserialize, Clone, Debug)]
1164pub struct ValidationSession {
1165 id: ValidationSessionID,
1166 description: String,
1167 dataset_id: DatasetID,
1168 experiment_id: ExperimentID,
1169 training_session_id: TrainingSessionID,
1170 #[serde(rename = "gt_annotation_set_id")]
1171 annotation_set_id: AnnotationSetID,
1172 #[serde(deserialize_with = "validation_session_params")]
1173 params: HashMap<String, Parameter>,
1174 #[serde(rename = "docker_task")]
1175 task: Task,
1176}
1177
1178fn validation_session_params<'de, D>(
1179 deserializer: D,
1180) -> Result<HashMap<String, Parameter>, D::Error>
1181where
1182 D: Deserializer<'de>,
1183{
1184 #[derive(Deserialize)]
1185 struct ModelParams {
1186 validation: Option<HashMap<String, Parameter>>,
1187 }
1188
1189 #[derive(Deserialize)]
1190 struct ValidateParams {
1191 model: String,
1192 }
1193
1194 #[derive(Deserialize)]
1195 struct Params {
1196 model_params: ModelParams,
1197 validate_params: ValidateParams,
1198 }
1199
1200 let params = Params::deserialize(deserializer)?;
1201 let params = match params.model_params.validation {
1202 Some(mut map) => {
1203 map.insert(
1204 "model".to_string(),
1205 Parameter::String(params.validate_params.model),
1206 );
1207 map
1208 }
1209 None => HashMap::from([(
1210 "model".to_string(),
1211 Parameter::String(params.validate_params.model),
1212 )]),
1213 };
1214
1215 Ok(params)
1216}
1217
1218impl ValidationSession {
1219 pub fn id(&self) -> ValidationSessionID {
1220 self.id
1221 }
1222
1223 pub fn name(&self) -> &str {
1224 self.task.name()
1225 }
1226
1227 pub fn description(&self) -> &str {
1228 &self.description
1229 }
1230
1231 pub fn dataset_id(&self) -> DatasetID {
1232 self.dataset_id
1233 }
1234
1235 pub fn experiment_id(&self) -> ExperimentID {
1236 self.experiment_id
1237 }
1238
1239 pub fn training_session_id(&self) -> TrainingSessionID {
1240 self.training_session_id
1241 }
1242
1243 pub fn annotation_set_id(&self) -> AnnotationSetID {
1244 self.annotation_set_id
1245 }
1246
1247 pub fn params(&self) -> &HashMap<String, Parameter> {
1248 &self.params
1249 }
1250
1251 pub fn task(&self) -> &Task {
1252 &self.task
1253 }
1254
1255 pub async fn metrics(
1256 &self,
1257 client: &client::Client,
1258 ) -> Result<HashMap<String, Parameter>, Error> {
1259 #[derive(Deserialize)]
1260 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1261 enum Response {
1262 Empty {},
1263 Map(HashMap<String, Parameter>),
1264 String(String),
1265 }
1266
1267 let params = HashMap::from([("validate_session_id", self.id().value())]);
1268 let resp: Response = client
1269 .rpc("validate.session.metrics".to_owned(), Some(params))
1270 .await?;
1271
1272 Ok(match resp {
1273 Response::String(metrics) => serde_json::from_str(&metrics)?,
1274 Response::Map(metrics) => metrics,
1275 Response::Empty {} => HashMap::new(),
1276 })
1277 }
1278
1279 pub async fn set_metrics(
1280 &self,
1281 client: &client::Client,
1282 metrics: HashMap<String, Parameter>,
1283 ) -> Result<(), Error> {
1284 let metrics = PublishMetrics {
1285 trainer_session_id: None,
1286 validate_session_id: Some(self.id()),
1287 metrics,
1288 };
1289
1290 let _: String = client
1291 .rpc("validate.session.metrics".to_owned(), Some(metrics))
1292 .await?;
1293
1294 Ok(())
1295 }
1296
1297 pub async fn upload_data(
1322 &self,
1323 client: &client::Client,
1324 files: &[(String, std::path::PathBuf)],
1325 folder: Option<&str>,
1326 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1327 ) -> Result<(), Error> {
1328 use futures::StreamExt;
1329 use std::sync::{
1330 Arc,
1331 atomic::{AtomicUsize, Ordering},
1332 };
1333 use tokio_util::io::ReaderStream;
1334
1335 let mut total: usize = 0;
1337 let mut file_meta = Vec::with_capacity(files.len());
1338 for (name, path) in files {
1339 let f = tokio::fs::File::open(path).await?;
1340 let len = f.metadata().await?.len() as usize;
1341 total += len;
1342 file_meta.push((name.clone(), f, len));
1343 }
1344
1345 let sent = Arc::new(AtomicUsize::new(0));
1347
1348 let mut form = Form::new().text("session_id", self.id().value().to_string());
1349 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
1350 form = form.text("folder", folder.to_owned());
1351 }
1352
1353 for (name, file, len) in file_meta {
1354 let reader_stream = ReaderStream::new(file);
1355 let sent_clone = sent.clone();
1356 let progress_clone = progress.clone();
1357 let progress_stream = reader_stream.inspect(move |chunk_result| {
1358 if let Ok(chunk) = chunk_result {
1359 let current =
1360 sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1361 if let Some(tx) = &progress_clone {
1366 let _ = tx.try_send(Progress {
1367 current,
1368 total,
1369 status: None,
1370 });
1371 }
1372 }
1373 });
1374 let body = reqwest::Body::wrap_stream(progress_stream);
1375 let part = Part::stream_with_length(body, len as u64).file_name(name);
1376 form = form.part("file", part);
1377 }
1378
1379 let result = match client.post_multipart("val.data.upload", form).await {
1380 Ok(_) => Ok(()),
1381 Err(Error::RpcError(code, msg)) => {
1382 Err(client::map_rpc_error("val.data.upload", code, msg, None))
1383 }
1384 Err(e) => Err(e),
1385 };
1386
1387 if result.is_ok()
1392 && let Some(tx) = progress
1393 {
1394 let _ = tx
1395 .send(Progress {
1396 current: total,
1397 total,
1398 status: None,
1399 })
1400 .await;
1401 }
1402 result
1403 }
1404
1405 pub async fn download_data(
1425 &self,
1426 client: &client::Client,
1427 filename: &str,
1428 output_path: &std::path::Path,
1429 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1430 ) -> Result<(), Error> {
1431 let req = client::ValDataDownloadRequest {
1432 session_id: self.id().value(),
1433 filename: filename.to_owned(),
1434 };
1435 match client
1436 .rpc_download("val.data.download", &req, output_path, progress)
1437 .await
1438 {
1439 Ok(()) => Ok(()),
1440 Err(Error::RpcError(code, msg)) => {
1441 Err(client::map_rpc_error("val.data.download", code, msg, None))
1442 }
1443 Err(e) => Err(e),
1444 }
1445 }
1446
1447 pub async fn data_list(&self, client: &client::Client) -> Result<Vec<String>, Error> {
1462 let req = client::ValDataListRequest {
1463 session_id: self.id().value(),
1464 };
1465 match client.rpc("val.data.list".to_owned(), Some(&req)).await {
1466 Ok(r) => Ok(r),
1467 Err(Error::RpcError(code, msg)) => {
1468 Err(client::map_rpc_error("val.data.list", code, msg, None))
1469 }
1470 Err(e) => Err(e),
1471 }
1472 }
1473}
1474
1475#[derive(Debug, Clone)]
1494pub struct StartValidationRequest {
1495 pub project_id: ProjectID,
1496 pub name: String,
1497 pub training_session_id: TrainingSessionID,
1498 pub model_file: String,
1499 pub val_type: String,
1500 pub params: HashMap<String, Parameter>,
1501 pub is_local: bool,
1502 pub is_kubernetes: bool,
1503 pub description: Option<String>,
1504 pub dataset_id: Option<DatasetID>,
1505 pub annotation_set_id: Option<AnnotationSetID>,
1506 pub snapshot_id: Option<SnapshotID>,
1507}
1508
1509#[derive(Deserialize, Debug, Clone)]
1524pub struct NewValidationSession {
1525 #[serde(rename = "id")]
1526 pub task_id: TaskID,
1527 #[serde(rename = "val_session_id", default)]
1528 pub session_id: Option<ValidationSessionID>,
1529}
1530
1531impl Display for NewValidationSession {
1532 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1533 match self.session_id {
1534 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1535 None => write!(f, "task {} (no session)", self.task_id),
1536 }
1537 }
1538}
1539
1540#[derive(Debug, Clone)]
1560pub struct StartTrainingRequest {
1561 pub project_id: ProjectID,
1563 pub name: String,
1565 pub experiment_id: ExperimentID,
1567 pub trainer_type: String,
1570 pub dataset_id: DatasetID,
1572 pub annotation_set_id: AnnotationSetID,
1574 pub tag_name: Option<String>,
1578 pub train_group: Option<String>,
1580 pub val_group: Option<String>,
1582 pub session_name: Option<String>,
1585 pub session_description: Option<String>,
1587 pub weights_session: Option<TrainingSessionID>,
1589 pub params: HashMap<String, Parameter>,
1591 pub is_local: bool,
1593 pub is_kubernetes: bool,
1595}
1596
1597#[derive(Deserialize, Debug, Clone)]
1610pub struct NewTrainingSession {
1611 #[serde(rename = "id")]
1612 pub task_id: TaskID,
1613 #[serde(rename = "train_session_id", default)]
1614 pub session_id: Option<TrainingSessionID>,
1615}
1616
1617impl Display for NewTrainingSession {
1618 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1619 match self.session_id {
1620 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1621 None => write!(f, "task {} (no session)", self.task_id),
1622 }
1623 }
1624}
1625
1626#[derive(Deserialize, Debug, Clone)]
1634pub struct Tag {
1635 pub id: u64,
1637 pub name: String,
1639 #[serde(default)]
1641 pub dataset_id: u64,
1642}
1643
1644#[derive(Deserialize, Clone, Debug, Default)]
1655#[serde(default)]
1656pub struct DatasetParams {
1657 dataset_id: DatasetID,
1658 annotation_set_id: AnnotationSetID,
1659 #[serde(rename = "train_group_name")]
1660 train_group: String,
1661 #[serde(rename = "val_group_name")]
1662 val_group: String,
1663}
1664
1665impl DatasetParams {
1666 pub fn dataset_id(&self) -> DatasetID {
1667 self.dataset_id
1668 }
1669
1670 pub fn annotation_set_id(&self) -> AnnotationSetID {
1671 self.annotation_set_id
1672 }
1673
1674 pub fn train_group(&self) -> &str {
1675 &self.train_group
1676 }
1677
1678 pub fn val_group(&self) -> &str {
1679 &self.val_group
1680 }
1681}
1682
1683#[derive(Serialize, Debug, Clone)]
1684pub struct TasksListParams {
1685 #[serde(skip_serializing_if = "Option::is_none")]
1686 pub continue_token: Option<String>,
1687 #[serde(skip_serializing_if = "Option::is_none")]
1688 pub types: Option<Vec<String>>,
1689 #[serde(rename = "manage_types", skip_serializing_if = "Option::is_none")]
1690 pub manager: Option<Vec<String>>,
1691 #[serde(skip_serializing_if = "Option::is_none")]
1692 pub status: Option<Vec<String>>,
1693}
1694
1695#[derive(Debug, Clone, Serialize, Deserialize)]
1701pub struct TaskDataList {
1702 pub server: String,
1703 #[serde(rename = "organization_uid")]
1704 pub organization_uid: String,
1705 #[serde(default)]
1706 pub traces: Vec<String>,
1707 #[serde(default)]
1708 pub data: std::collections::HashMap<String, Vec<String>>,
1709}
1710
1711#[derive(Debug, Clone, Serialize, Deserialize)]
1716pub struct Job {
1717 #[serde(default)]
1719 pub code: String,
1720 #[serde(default)]
1722 pub title: String,
1723 #[serde(default)]
1725 pub job_name: String,
1726 #[serde(default)]
1728 pub job_id: String,
1729 #[serde(default)]
1731 pub state: String,
1732 #[serde(default)]
1734 pub launch: Option<DateTime<Utc>>,
1735 pub task_id: i64,
1740}
1741
1742impl Job {
1743 pub fn task_id(&self) -> TaskID {
1749 TaskID::from(self.task_id.max(0) as u64)
1750 }
1751}
1752
1753#[derive(Deserialize, Debug, Clone)]
1754pub struct TasksListResult {
1755 pub tasks: Vec<Task>,
1756 pub continue_token: Option<String>,
1757}
1758
1759#[derive(Deserialize, Debug, Clone)]
1760pub struct Task {
1761 id: TaskID,
1762 name: String,
1763 #[serde(rename = "type")]
1764 workflow: String,
1765 status: String,
1766 #[serde(rename = "manage_type")]
1767 manager: Option<String>,
1768 #[serde(rename = "instance_type")]
1769 instance: String,
1770 #[serde(rename = "date")]
1771 created: DateTime<Utc>,
1772}
1773
1774impl Task {
1775 pub fn id(&self) -> TaskID {
1776 self.id
1777 }
1778
1779 pub fn name(&self) -> &str {
1780 &self.name
1781 }
1782
1783 pub fn workflow(&self) -> &str {
1784 &self.workflow
1785 }
1786
1787 pub fn status(&self) -> &str {
1788 &self.status
1789 }
1790
1791 pub fn manager(&self) -> Option<&str> {
1792 self.manager.as_deref()
1793 }
1794
1795 pub fn instance(&self) -> &str {
1796 &self.instance
1797 }
1798
1799 pub fn created(&self) -> &DateTime<Utc> {
1800 &self.created
1801 }
1802}
1803
1804impl Display for Task {
1805 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1806 write!(
1807 f,
1808 "{} [{:?} {}] {}",
1809 self.id,
1810 self.manager(),
1811 self.workflow(),
1812 self.name()
1813 )
1814 }
1815}
1816
1817#[derive(Deserialize, Debug, Clone)]
1818pub struct TaskInfo {
1819 id: TaskID,
1820 project_id: Option<ProjectID>,
1821 #[serde(rename = "task_description", alias = "description", default)]
1822 description: String,
1823 #[serde(rename = "type")]
1824 workflow: String,
1825 status: Option<String>,
1826 #[serde(default)]
1827 progress: TaskProgress,
1828 #[serde(
1829 rename = "created_date",
1830 alias = "created",
1831 default = "default_datetime_utc"
1832 )]
1833 created: DateTime<Utc>,
1834 #[serde(
1835 rename = "end_date",
1836 alias = "completed",
1837 default = "default_datetime_utc"
1838 )]
1839 completed: DateTime<Utc>,
1840}
1841
1842fn default_datetime_utc() -> DateTime<Utc> {
1843 DateTime::UNIX_EPOCH
1844}
1845
1846impl Display for TaskInfo {
1847 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1848 write!(f, "{} {}: {}", self.id, self.workflow(), self.description())
1849 }
1850}
1851
1852impl TaskInfo {
1853 pub fn id(&self) -> TaskID {
1854 self.id
1855 }
1856
1857 pub fn project_id(&self) -> Option<ProjectID> {
1858 self.project_id
1859 }
1860
1861 pub fn description(&self) -> &str {
1862 &self.description
1863 }
1864
1865 pub fn workflow(&self) -> &str {
1866 &self.workflow
1867 }
1868
1869 pub fn status(&self) -> &Option<String> {
1870 &self.status
1871 }
1872
1873 pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1874 let t = client.task_status(self.id(), status).await?;
1875 self.status = Some(t.status);
1876 Ok(())
1877 }
1878
1879 pub fn stages(&self) -> HashMap<String, Stage> {
1880 match &self.progress.stages {
1881 Some(stages) => stages.clone(),
1882 None => HashMap::new(),
1883 }
1884 }
1885
1886 pub async fn update_stage(
1887 &mut self,
1888 client: &Client,
1889 stage: &str,
1890 status: &str,
1891 message: &str,
1892 percentage: u8,
1893 ) -> Result<(), Error> {
1894 client
1895 .update_stage(self.id(), stage, status, message, percentage)
1896 .await?;
1897 let t = client.task_info(self.id()).await?;
1898 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1899 Ok(())
1900 }
1901
1902 pub async fn set_stages(
1903 &mut self,
1904 client: &Client,
1905 stages: &[(&str, &str)],
1906 ) -> Result<(), Error> {
1907 client.set_stages(self.id(), stages).await?;
1908 let t = client.task_info(self.id()).await?;
1909 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1910 Ok(())
1911 }
1912
1913 pub async fn data_list(&self, client: &client::Client) -> Result<TaskDataList, Error> {
1929 let req = client::TaskDataListRequest {
1930 task_id: self.id().value(),
1931 };
1932 match client.rpc("task.data.list".to_owned(), Some(&req)).await {
1933 Ok(r) => Ok(r),
1934 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1935 "task.data.list",
1936 code,
1937 msg,
1938 Some(self.id()),
1939 )),
1940 Err(e) => Err(e),
1941 }
1942 }
1943
1944 pub async fn upload_data(
1965 &self,
1966 client: &client::Client,
1967 path: &std::path::Path,
1968 folder: Option<&str>,
1969 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1970 ) -> Result<(), Error> {
1971 use futures::StreamExt;
1972 use std::sync::{
1973 Arc,
1974 atomic::{AtomicUsize, Ordering},
1975 };
1976 use tokio_util::io::ReaderStream;
1977
1978 let file_name = path
1979 .file_name()
1980 .and_then(|s| s.to_str())
1981 .ok_or_else(|| Error::InvalidParameters("path must have a UTF-8 filename".into()))?
1982 .to_owned();
1983
1984 let file = tokio::fs::File::open(path).await?;
1985 let total = file.metadata().await?.len() as usize;
1986 let sent = Arc::new(AtomicUsize::new(0));
1987
1988 let reader_stream = ReaderStream::new(file);
1989 let sent_clone = sent.clone();
1990 let progress_clone = progress.clone();
1991 let progress_stream = reader_stream.inspect(move |chunk_result| {
1992 if let Ok(chunk) = chunk_result {
1993 let current = sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1994 if let Some(tx) = &progress_clone {
2000 let _ = tx.try_send(Progress {
2001 current,
2002 total,
2003 status: None,
2004 });
2005 }
2006 }
2007 });
2008
2009 let body = reqwest::Body::wrap_stream(progress_stream);
2010 let file_part = Part::stream_with_length(body, total as u64).file_name(file_name);
2011
2012 let mut form = Form::new().text("task_id", self.id().value().to_string());
2013 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
2014 form = form.text("folder", folder.to_owned());
2015 }
2016 form = form.part("file", file_part);
2017
2018 let result = match client.post_multipart("task.data.upload", form).await {
2019 Ok(_) => Ok(()),
2020 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2021 "task.data.upload",
2022 code,
2023 msg,
2024 Some(self.id()),
2025 )),
2026 Err(e) => Err(e),
2027 };
2028
2029 if result.is_ok()
2033 && let Some(tx) = progress
2034 {
2035 let _ = tx
2036 .send(Progress {
2037 current: total,
2038 total,
2039 status: None,
2040 })
2041 .await;
2042 }
2043 result
2044 }
2045
2046 pub async fn download_data(
2075 &self,
2076 client: &client::Client,
2077 file: &str,
2078 folder: Option<&str>,
2079 output_path: &std::path::Path,
2080 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
2081 ) -> Result<(), Error> {
2082 let folder = folder.unwrap_or("").to_owned();
2083 let req = client::TaskDataDownloadRequest {
2084 task_id: self.id().value(),
2085 folder,
2086 file: file.to_owned(),
2087 };
2088 match client
2089 .rpc_download("task.data.download", &req, output_path, progress)
2090 .await
2091 {
2092 Ok(()) => Ok(()),
2093 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2094 "task.data.download",
2095 code,
2096 msg,
2097 Some(self.id()),
2098 )),
2099 Err(e) => Err(e),
2100 }
2101 }
2102
2103 pub async fn add_chart(
2131 &self,
2132 client: &client::Client,
2133 group: &str,
2134 name: &str,
2135 data: Parameter,
2136 params: Option<Parameter>,
2137 ) -> Result<(), Error> {
2138 client::validate_chart_args(group, name)?;
2139 let req = client::TaskChartAddRequest {
2140 task_id: self.id().value(),
2141 group_name: group.to_owned(),
2142 chart_name: name.to_owned(),
2143 params,
2144 data,
2145 };
2146 let _resp: serde_json::Value =
2147 match client.rpc("task.chart.add".to_owned(), Some(&req)).await {
2148 Ok(r) => r,
2149 Err(Error::RpcError(code, msg)) => {
2150 return Err(client::map_rpc_error(
2151 "task.chart.add",
2152 code,
2153 msg,
2154 Some(self.id()),
2155 ));
2156 }
2157 Err(e) => return Err(e),
2158 };
2159 Ok(())
2160 }
2161
2162 pub async fn list_charts(
2179 &self,
2180 client: &client::Client,
2181 group: Option<&str>,
2182 ) -> Result<TaskDataList, Error> {
2183 let req = client::TaskChartListRequest {
2184 task_id: self.id().value(),
2185 group_name: group.unwrap_or("").to_owned(),
2186 };
2187 match client.rpc("task.chart.list".to_owned(), Some(&req)).await {
2188 Ok(r) => Ok(r),
2189 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2190 "task.chart.list",
2191 code,
2192 msg,
2193 Some(self.id()),
2194 )),
2195 Err(e) => Err(e),
2196 }
2197 }
2198
2199 pub async fn get_chart(
2218 &self,
2219 client: &client::Client,
2220 group: &str,
2221 name: &str,
2222 ) -> Result<Parameter, Error> {
2223 client::validate_chart_args(group, name)?;
2224 let req = client::TaskChartGetRequest {
2225 task_id: self.id().value(),
2226 group_name: group.to_owned(),
2227 chart_name: name.to_owned(),
2228 };
2229 match client.rpc("task.chart.get".to_owned(), Some(&req)).await {
2230 Ok(r) => Ok(r),
2231 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2232 "task.chart.get",
2233 code,
2234 msg,
2235 Some(self.id()),
2236 )),
2237 Err(e) => Err(e),
2238 }
2239 }
2240
2241 pub fn created(&self) -> &DateTime<Utc> {
2242 &self.created
2243 }
2244
2245 pub fn completed(&self) -> &DateTime<Utc> {
2246 &self.completed
2247 }
2248}
2249
2250#[derive(Deserialize, Debug, Default, Clone)]
2251pub struct TaskProgress {
2252 stages: Option<HashMap<String, Stage>>,
2253}
2254
2255#[derive(Serialize, Debug, Clone)]
2256pub struct TaskStatus {
2257 #[serde(rename = "docker_task_id")]
2258 pub task_id: TaskID,
2259 pub status: String,
2260}
2261
2262#[derive(Serialize, Deserialize, Debug, Clone)]
2263pub struct Stage {
2264 #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
2265 task_id: Option<TaskID>,
2266 stage: String,
2267 #[serde(skip_serializing_if = "Option::is_none")]
2268 status: Option<String>,
2269 #[serde(skip_serializing_if = "Option::is_none")]
2270 description: Option<String>,
2271 #[serde(skip_serializing_if = "Option::is_none")]
2272 message: Option<String>,
2273 percentage: u8,
2274}
2275
2276impl Stage {
2277 pub fn new(
2278 task_id: Option<TaskID>,
2279 stage: String,
2280 status: Option<String>,
2281 message: Option<String>,
2282 percentage: u8,
2283 ) -> Self {
2284 Stage {
2285 task_id,
2286 stage,
2287 status,
2288 description: None,
2289 message,
2290 percentage,
2291 }
2292 }
2293
2294 pub fn task_id(&self) -> &Option<TaskID> {
2295 &self.task_id
2296 }
2297
2298 pub fn stage(&self) -> &str {
2299 &self.stage
2300 }
2301
2302 pub fn status(&self) -> &Option<String> {
2303 &self.status
2304 }
2305
2306 pub fn description(&self) -> &Option<String> {
2307 &self.description
2308 }
2309
2310 pub fn message(&self) -> &Option<String> {
2311 &self.message
2312 }
2313
2314 pub fn percentage(&self) -> u8 {
2315 self.percentage
2316 }
2317}
2318
2319#[derive(Serialize, Debug)]
2320pub struct TaskStages {
2321 #[serde(rename = "docker_task_id")]
2322 pub task_id: TaskID,
2323 #[serde(skip_serializing_if = "Vec::is_empty")]
2324 pub stages: Vec<HashMap<String, String>>,
2325}
2326
2327#[derive(Deserialize, Debug)]
2328pub struct Artifact {
2329 name: String,
2330 #[serde(rename = "modelType")]
2331 model_type: String,
2332}
2333
2334impl Artifact {
2335 pub fn name(&self) -> &str {
2336 &self.name
2337 }
2338
2339 pub fn model_type(&self) -> &str {
2340 &self.model_type
2341 }
2342}
2343
2344#[derive(Deserialize, Serialize, Clone, Debug)]
2352pub struct VersionTag {
2353 id: u64,
2354 dataset_id: DatasetID,
2355 name: String,
2356 serial: u64,
2357 #[serde(default)]
2358 description: String,
2359 created_by: String,
2360 created_at: DateTime<Utc>,
2361 #[serde(default)]
2362 image_count: u64,
2363 #[serde(default)]
2364 annotation_counts: HashMap<String, u64>,
2365 #[serde(default)]
2366 sensor_counts: HashMap<String, u64>,
2367 #[serde(default)]
2368 label_count: u64,
2369 #[serde(default)]
2370 annotation_set_count: u64,
2371 #[serde(default)]
2372 snapshot_id: Option<u64>,
2373 #[serde(default)]
2374 is_current: bool,
2375}
2376
2377impl VersionTag {
2378 pub fn id(&self) -> u64 {
2380 self.id
2381 }
2382
2383 pub fn dataset_id(&self) -> DatasetID {
2385 self.dataset_id
2386 }
2387
2388 pub fn name(&self) -> &str {
2390 &self.name
2391 }
2392
2393 pub fn serial(&self) -> u64 {
2395 self.serial
2396 }
2397
2398 pub fn description(&self) -> &str {
2400 &self.description
2401 }
2402
2403 pub fn created_by(&self) -> &str {
2405 &self.created_by
2406 }
2407
2408 pub fn created_at(&self) -> DateTime<Utc> {
2410 self.created_at
2411 }
2412
2413 pub fn image_count(&self) -> u64 {
2415 self.image_count
2416 }
2417
2418 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2420 &self.annotation_counts
2421 }
2422
2423 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2425 &self.sensor_counts
2426 }
2427
2428 pub fn label_count(&self) -> u64 {
2430 self.label_count
2431 }
2432
2433 pub fn annotation_set_count(&self) -> u64 {
2435 self.annotation_set_count
2436 }
2437
2438 pub fn snapshot_id(&self) -> Option<u64> {
2440 self.snapshot_id
2441 }
2442
2443 pub fn is_current(&self) -> bool {
2446 self.is_current
2447 }
2448}
2449
2450impl Display for VersionTag {
2451 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2452 write!(f, "{} (serial {})", self.name, self.serial)
2453 }
2454}
2455
2456#[derive(Deserialize, Serialize, Clone, Debug)]
2458pub struct ChangelogEntry {
2459 id: u64,
2460 dataset_id: DatasetID,
2461 serial: u64,
2462 entity_type: String,
2463 operation: String,
2464 #[serde(default)]
2465 entity_id: Option<u64>,
2466 #[serde(default)]
2467 change_data: serde_json::Value,
2468 username: String,
2469 organization_id: u64,
2470 created_at: DateTime<Utc>,
2471 #[serde(default)]
2472 message: String,
2473 #[serde(default, deserialize_with = "deserialize_null_as_default")]
2474 s3_version_ids: Vec<serde_json::Value>,
2475}
2476
2477impl ChangelogEntry {
2478 pub fn id(&self) -> u64 {
2479 self.id
2480 }
2481
2482 pub fn dataset_id(&self) -> DatasetID {
2483 self.dataset_id
2484 }
2485
2486 pub fn serial(&self) -> u64 {
2488 self.serial
2489 }
2490
2491 pub fn entity_type(&self) -> &str {
2493 &self.entity_type
2494 }
2495
2496 pub fn operation(&self) -> &str {
2498 &self.operation
2499 }
2500
2501 pub fn entity_id(&self) -> Option<u64> {
2502 self.entity_id
2503 }
2504
2505 pub fn change_data(&self) -> &serde_json::Value {
2507 &self.change_data
2508 }
2509
2510 pub fn username(&self) -> &str {
2511 &self.username
2512 }
2513
2514 pub fn organization_id(&self) -> u64 {
2515 self.organization_id
2516 }
2517
2518 pub fn created_at(&self) -> DateTime<Utc> {
2519 self.created_at
2520 }
2521
2522 pub fn message(&self) -> &str {
2523 &self.message
2524 }
2525
2526 pub fn s3_version_ids(&self) -> &[serde_json::Value] {
2527 &self.s3_version_ids
2528 }
2529}
2530
2531#[derive(Deserialize, Debug, Clone)]
2533pub struct ChangelogResponse {
2534 pub entries: Vec<ChangelogEntry>,
2535 pub count: u64,
2536 #[serde(default)]
2537 pub continue_token: String,
2538 #[serde(default)]
2539 pub from_serial: Option<u64>,
2540 #[serde(default)]
2541 pub to_serial: Option<u64>,
2542}
2543
2544#[derive(Deserialize, Serialize, Clone, Debug)]
2546pub struct DatasetSummary {
2547 dataset_id: DatasetID,
2548 current_serial: u64,
2549 #[serde(default)]
2550 image_count: u64,
2551 #[serde(default)]
2552 annotation_counts: HashMap<String, u64>,
2553 #[serde(default)]
2554 sensor_counts: HashMap<String, u64>,
2555 #[serde(default)]
2556 label_count: u64,
2557 #[serde(default)]
2558 annotation_set_count: u64,
2559 last_updated: DateTime<Utc>,
2560}
2561
2562impl DatasetSummary {
2563 pub fn dataset_id(&self) -> DatasetID {
2564 self.dataset_id
2565 }
2566
2567 pub fn current_serial(&self) -> u64 {
2568 self.current_serial
2569 }
2570
2571 pub fn image_count(&self) -> u64 {
2572 self.image_count
2573 }
2574
2575 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2576 &self.annotation_counts
2577 }
2578
2579 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2580 &self.sensor_counts
2581 }
2582
2583 pub fn label_count(&self) -> u64 {
2584 self.label_count
2585 }
2586
2587 pub fn annotation_set_count(&self) -> u64 {
2588 self.annotation_set_count
2589 }
2590
2591 pub fn last_updated(&self) -> DateTime<Utc> {
2592 self.last_updated
2593 }
2594}
2595
2596#[derive(Deserialize, Debug, Clone)]
2598pub struct VersionCurrentResponse {
2599 pub dataset_id: DatasetID,
2600 pub current_serial: u64,
2601 #[serde(default)]
2602 pub latest_tag: Option<VersionTag>,
2603 #[serde(default)]
2604 pub tags: Vec<VersionTag>,
2605 #[serde(default)]
2606 pub summary: Option<DatasetSummary>,
2607}
2608
2609#[derive(Deserialize, Debug, Clone)]
2611pub struct RestoredFrom {
2612 pub tag: String,
2613 pub serial: u64,
2614}
2615
2616#[derive(Deserialize, Debug, Clone)]
2618pub struct RestoredCounts {
2619 pub images: u64,
2620 pub labels: u64,
2621 pub annotation_sets: u64,
2622}
2623
2624#[derive(Deserialize, Debug, Clone)]
2626pub struct RestoreResult {
2627 pub success: bool,
2628 pub new_serial: u64,
2629 pub restored_from: RestoredFrom,
2630 pub restored_counts: RestoredCounts,
2631 pub message: String,
2632}
2633
2634#[derive(Serialize)]
2637pub(crate) struct VersionTagCreateParams {
2638 pub dataset_id: DatasetID,
2639 pub name: String,
2640 #[serde(skip_serializing_if = "Option::is_none")]
2641 pub description: Option<String>,
2642}
2643
2644#[derive(Serialize)]
2645pub(crate) struct VersionTagNameParams {
2646 pub dataset_id: DatasetID,
2647 pub name: String,
2648}
2649
2650#[derive(Serialize)]
2651pub(crate) struct VersionChangelogParams {
2652 pub dataset_id: DatasetID,
2653 #[serde(skip_serializing_if = "Option::is_none")]
2654 pub from_version: Option<String>,
2655 #[serde(skip_serializing_if = "Option::is_none")]
2656 pub to_version: Option<String>,
2657 #[serde(skip_serializing_if = "Option::is_none")]
2658 pub entity_types: Option<Vec<String>>,
2659 #[serde(skip_serializing_if = "Option::is_none")]
2660 pub limit: Option<u64>,
2661 #[serde(skip_serializing_if = "Option::is_none")]
2662 pub continue_token: Option<String>,
2663}
2664
2665#[derive(Deserialize, Debug)]
2667pub(crate) struct ChangelogCountResult {
2668 pub count: u64,
2669}
2670
2671#[derive(Serialize, Deserialize, Debug, Clone)]
2678pub struct TrainerSchemaInfo {
2679 pub name: String,
2681 #[serde(default)]
2683 pub label: String,
2684 #[serde(default)]
2686 pub schema_type: String,
2687}
2688
2689#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
2695#[serde(rename_all = "lowercase")]
2696pub enum SchemaFieldType {
2697 Group,
2699 Slider,
2701 Select,
2703 Bool,
2705 Int,
2707 Float,
2709 Text,
2711 Date,
2713 Project,
2715 Dataset,
2717 Trainer,
2719 Upload,
2721 Info,
2724 #[serde(other)]
2726 Unknown,
2727}
2728
2729fn lenient_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
2733where
2734 D: Deserializer<'de>,
2735{
2736 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
2737 Ok(value.map(|v| match v {
2738 serde_json::Value::String(s) => s,
2739 other => other.to_string(),
2740 }))
2741}
2742
2743#[derive(Serialize, Deserialize, Debug, Clone)]
2745pub struct SchemaOption {
2746 #[serde(default)]
2748 pub name: Option<Parameter>,
2749 #[serde(default, deserialize_with = "lenient_string")]
2752 pub label: Option<String>,
2753 #[serde(default)]
2755 pub children: Vec<SchemaField>,
2756}
2757
2758#[derive(Serialize, Deserialize, Debug, Clone)]
2770pub struct SchemaField {
2771 #[serde(default, deserialize_with = "lenient_string")]
2773 pub name: Option<String>,
2774 #[serde(default, deserialize_with = "lenient_string")]
2776 pub label: Option<String>,
2777 #[serde(default, deserialize_with = "lenient_string")]
2779 pub description: Option<String>,
2780 #[serde(default)]
2782 pub required: bool,
2783 #[serde(default)]
2785 pub default: Option<Parameter>,
2786 #[serde(rename = "type", default)]
2788 pub field_type: Option<SchemaFieldType>,
2789 #[serde(default)]
2791 pub min: Option<f64>,
2792 #[serde(default)]
2794 pub max: Option<f64>,
2795 #[serde(default)]
2797 pub step: Option<f64>,
2798 #[serde(default)]
2800 pub options: Vec<SchemaOption>,
2801 #[serde(default)]
2804 pub children: Vec<SchemaField>,
2805 #[serde(default)]
2807 pub is_dropdown: bool,
2808 #[serde(default)]
2810 pub multi_select: bool,
2811 #[serde(default)]
2813 pub is_multi_line: bool,
2814 #[serde(default)]
2816 pub hidden: bool,
2817 #[serde(default)]
2819 pub numeric_only: bool,
2820 #[serde(default)]
2822 pub enable_tags_selection: bool,
2823 #[serde(default)]
2825 pub enable_annotation_set_selection: bool,
2826 #[serde(default)]
2828 pub values: Option<Vec<Parameter>>,
2829}
2830
2831#[derive(Serialize, Deserialize, Debug, Clone)]
2834pub struct ValidatorSchema {
2835 #[serde(rename = "type", default)]
2837 pub schema_type: String,
2838 #[serde(default)]
2840 pub name: String,
2841 #[serde(default)]
2843 pub schema: Vec<SchemaField>,
2844}
2845
2846#[cfg(test)]
2847mod tests {
2848 use super::*;
2849
2850 #[test]
2852 fn test_organization_id_from_u64() {
2853 let id = OrganizationID::from(12345);
2854 assert_eq!(id.value(), 12345);
2855 }
2856
2857 #[test]
2858 fn test_organization_id_display() {
2859 let id = OrganizationID::from(0xabc123);
2860 assert_eq!(format!("{}", id), "org-abc123");
2861 }
2862
2863 #[test]
2864 fn test_organization_id_try_from_str_valid() {
2865 let id = OrganizationID::try_from("org-abc123").unwrap();
2866 assert_eq!(id.value(), 0xabc123);
2867 }
2868
2869 #[test]
2870 fn test_organization_id_try_from_str_invalid_prefix() {
2871 let result = OrganizationID::try_from("invalid-abc123");
2872 assert!(result.is_err());
2873 match result {
2874 Err(Error::InvalidParameters(msg)) => {
2875 assert!(msg.contains("must start with 'org-'"));
2876 }
2877 _ => panic!("Expected InvalidParameters error"),
2878 }
2879 }
2880
2881 #[test]
2882 fn test_organization_id_try_from_str_invalid_hex() {
2883 let result = OrganizationID::try_from("org-xyz");
2884 assert!(result.is_err());
2885 }
2886
2887 #[test]
2888 fn test_organization_id_try_from_str_empty() {
2889 let result = OrganizationID::try_from("org-");
2890 assert!(result.is_err());
2891 }
2892
2893 #[test]
2894 fn test_organization_id_into_u64() {
2895 let id = OrganizationID::from(54321);
2896 let value: u64 = id.into();
2897 assert_eq!(value, 54321);
2898 }
2899
2900 #[test]
2902 fn test_usage_summary_deserialize_and_accessors() {
2903 let usage: UsageSummary = serde_json::from_str(
2904 r#"{"credits": 12.5, "funds": 49092.92, "total_funds_and_credits": 49105.42}"#,
2905 )
2906 .unwrap();
2907 assert_eq!(usage.credits(), 12.5);
2908 assert_eq!(usage.funds(), 49092.92);
2909 assert_eq!(usage.total(), 49105.42);
2910 }
2911
2912 #[test]
2913 fn test_usage_summary_defaults_for_missing_fields() {
2914 let usage: UsageSummary = serde_json::from_str("{}").unwrap();
2918 assert_eq!(usage.credits(), 0.0);
2919 assert_eq!(usage.funds(), 0.0);
2920 assert_eq!(usage.total(), 0.0);
2921 }
2922
2923 #[test]
2925 fn test_project_id_from_u64() {
2926 let id = ProjectID::from(78910);
2927 assert_eq!(id.value(), 78910);
2928 }
2929
2930 #[test]
2931 fn test_project_id_display() {
2932 let id = ProjectID::from(0xdef456);
2933 assert_eq!(format!("{}", id), "p-def456");
2934 }
2935
2936 #[test]
2937 fn test_project_id_from_str_valid() {
2938 let id = ProjectID::from_str("p-def456").unwrap();
2939 assert_eq!(id.value(), 0xdef456);
2940 }
2941
2942 #[test]
2943 fn test_project_id_try_from_str_valid() {
2944 let id = ProjectID::try_from("p-123abc").unwrap();
2945 assert_eq!(id.value(), 0x123abc);
2946 }
2947
2948 #[test]
2949 fn test_project_id_try_from_string_valid() {
2950 let id = ProjectID::try_from("p-456def".to_string()).unwrap();
2951 assert_eq!(id.value(), 0x456def);
2952 }
2953
2954 #[test]
2955 fn test_project_id_from_str_invalid_prefix() {
2956 let result = ProjectID::from_str("proj-123");
2957 assert!(result.is_err());
2958 match result {
2959 Err(Error::InvalidParameters(msg)) => {
2960 assert!(msg.contains("must start with 'p-'"));
2961 }
2962 _ => panic!("Expected InvalidParameters error"),
2963 }
2964 }
2965
2966 #[test]
2967 fn test_project_id_from_str_invalid_hex() {
2968 let result = ProjectID::from_str("p-notahex");
2969 assert!(result.is_err());
2970 }
2971
2972 #[test]
2973 fn test_project_id_into_u64() {
2974 let id = ProjectID::from(99999);
2975 let value: u64 = id.into();
2976 assert_eq!(value, 99999);
2977 }
2978
2979 #[test]
2981 fn test_experiment_id_from_u64() {
2982 let id = ExperimentID::from(1193046);
2983 assert_eq!(id.value(), 1193046);
2984 }
2985
2986 #[test]
2987 fn test_experiment_id_display() {
2988 let id = ExperimentID::from(0x123abc);
2989 assert_eq!(format!("{}", id), "exp-123abc");
2990 }
2991
2992 #[test]
2993 fn test_experiment_id_from_str_valid() {
2994 let id = ExperimentID::from_str("exp-456def").unwrap();
2995 assert_eq!(id.value(), 0x456def);
2996 }
2997
2998 #[test]
2999 fn test_experiment_id_try_from_str_valid() {
3000 let id = ExperimentID::try_from("exp-789abc").unwrap();
3001 assert_eq!(id.value(), 0x789abc);
3002 }
3003
3004 #[test]
3005 fn test_experiment_id_try_from_string_valid() {
3006 let id = ExperimentID::try_from("exp-fedcba".to_string()).unwrap();
3007 assert_eq!(id.value(), 0xfedcba);
3008 }
3009
3010 #[test]
3011 fn test_experiment_id_from_str_invalid_prefix() {
3012 let result = ExperimentID::from_str("experiment-123");
3013 assert!(result.is_err());
3014 match result {
3015 Err(Error::InvalidParameters(msg)) => {
3016 assert!(msg.contains("must start with 'exp-'"));
3017 }
3018 _ => panic!("Expected InvalidParameters error"),
3019 }
3020 }
3021
3022 #[test]
3023 fn test_experiment_id_from_str_invalid_hex() {
3024 let result = ExperimentID::from_str("exp-zzz");
3025 assert!(result.is_err());
3026 }
3027
3028 #[test]
3029 fn test_experiment_id_into_u64() {
3030 let id = ExperimentID::from(777777);
3031 let value: u64 = id.into();
3032 assert_eq!(value, 777777);
3033 }
3034
3035 #[test]
3037 fn test_training_session_id_from_u64() {
3038 let id = TrainingSessionID::from(7901234);
3039 assert_eq!(id.value(), 7901234);
3040 }
3041
3042 #[test]
3043 fn test_training_session_id_display() {
3044 let id = TrainingSessionID::from(0xabc123);
3045 assert_eq!(format!("{}", id), "t-abc123");
3046 }
3047
3048 #[test]
3049 fn test_training_session_id_from_str_valid() {
3050 let id = TrainingSessionID::from_str("t-abc123").unwrap();
3051 assert_eq!(id.value(), 0xabc123);
3052 }
3053
3054 #[test]
3055 fn test_training_session_id_try_from_str_valid() {
3056 let id = TrainingSessionID::try_from("t-deadbeef").unwrap();
3057 assert_eq!(id.value(), 0xdeadbeef);
3058 }
3059
3060 #[test]
3061 fn test_training_session_id_try_from_string_valid() {
3062 let id = TrainingSessionID::try_from("t-cafebabe".to_string()).unwrap();
3063 assert_eq!(id.value(), 0xcafebabe);
3064 }
3065
3066 #[test]
3067 fn test_training_session_id_from_str_invalid_prefix() {
3068 let result = TrainingSessionID::from_str("training-123");
3069 assert!(result.is_err());
3070 match result {
3071 Err(Error::InvalidParameters(msg)) => {
3072 assert!(msg.contains("must start with 't-'"));
3073 }
3074 _ => panic!("Expected InvalidParameters error"),
3075 }
3076 }
3077
3078 #[test]
3079 fn test_training_session_id_from_str_invalid_hex() {
3080 let result = TrainingSessionID::from_str("t-qqq");
3081 assert!(result.is_err());
3082 }
3083
3084 #[test]
3085 fn test_training_session_id_into_u64() {
3086 let id = TrainingSessionID::from(123456);
3087 let value: u64 = id.into();
3088 assert_eq!(value, 123456);
3089 }
3090
3091 #[test]
3093 fn test_validation_session_id_from_u64() {
3094 let id = ValidationSessionID::from(3456789);
3095 assert_eq!(id.value(), 3456789);
3096 }
3097
3098 #[test]
3099 fn test_validation_session_id_display() {
3100 let id = ValidationSessionID::from(0x34c985);
3101 assert_eq!(format!("{}", id), "v-34c985");
3102 }
3103
3104 #[test]
3105 fn test_validation_session_id_try_from_str_valid() {
3106 let id = ValidationSessionID::try_from("v-deadbeef").unwrap();
3107 assert_eq!(id.value(), 0xdeadbeef);
3108 }
3109
3110 #[test]
3111 fn test_validation_session_id_try_from_string_valid() {
3112 let id = ValidationSessionID::try_from("v-12345678".to_string()).unwrap();
3113 assert_eq!(id.value(), 0x12345678);
3114 }
3115
3116 #[test]
3117 fn test_validation_session_id_try_from_str_invalid_prefix() {
3118 let result = ValidationSessionID::try_from("validation-123");
3119 assert!(result.is_err());
3120 match result {
3121 Err(Error::InvalidParameters(msg)) => {
3122 assert!(msg.contains("must start with 'v-'"));
3123 }
3124 _ => panic!("Expected InvalidParameters error"),
3125 }
3126 }
3127
3128 #[test]
3129 fn test_validation_session_id_try_from_str_invalid_hex() {
3130 let result = ValidationSessionID::try_from("v-xyz");
3131 assert!(result.is_err());
3132 }
3133
3134 #[test]
3135 fn test_validation_session_id_into_u64() {
3136 let id = ValidationSessionID::from(987654);
3137 let value: u64 = id.into();
3138 assert_eq!(value, 987654);
3139 }
3140
3141 #[test]
3143 fn test_snapshot_id_from_u64() {
3144 let id = SnapshotID::from(111222);
3145 assert_eq!(id.value(), 111222);
3146 }
3147
3148 #[test]
3149 fn test_snapshot_id_display() {
3150 let id = SnapshotID::from(0xaabbcc);
3151 assert_eq!(format!("{}", id), "ss-aabbcc");
3152 }
3153
3154 #[test]
3155 fn test_snapshot_id_try_from_str_valid() {
3156 let id = SnapshotID::try_from("ss-aabbcc").unwrap();
3157 assert_eq!(id.value(), 0xaabbcc);
3158 }
3159
3160 #[test]
3161 fn test_snapshot_id_try_from_str_invalid_prefix() {
3162 let result = SnapshotID::try_from("snapshot-123");
3163 assert!(result.is_err());
3164 match result {
3165 Err(Error::InvalidParameters(msg)) => {
3166 assert!(msg.contains("must start with 'ss-'"));
3167 }
3168 _ => panic!("Expected InvalidParameters error"),
3169 }
3170 }
3171
3172 #[test]
3173 fn test_snapshot_id_try_from_str_invalid_hex() {
3174 let result = SnapshotID::try_from("ss-ggg");
3175 assert!(result.is_err());
3176 }
3177
3178 #[test]
3179 fn test_snapshot_id_into_u64() {
3180 let id = SnapshotID::from(333444);
3181 let value: u64 = id.into();
3182 assert_eq!(value, 333444);
3183 }
3184
3185 #[test]
3188 fn test_background_task_id_parses_bt_prefix() {
3189 let id = BackgroundTaskID::from_str("bt-55b5").unwrap();
3192 assert_eq!(id.value(), 0x55b5);
3193 }
3194
3195 #[test]
3196 fn test_background_task_id_display_round_trips() {
3197 let id = BackgroundTaskID::from(0x55b5);
3198 assert_eq!(id.to_string(), "bt-55b5");
3199 assert_eq!(BackgroundTaskID::from_str("bt-55b5").unwrap(), id);
3200 }
3201
3202 #[test]
3203 fn test_background_task_id_rejects_other_prefixes() {
3204 for s in ["task-55b5", "55b5", "b-55b5", "bt55b5"] {
3208 let result = BackgroundTaskID::from_str(s);
3209 assert!(result.is_err(), "expected {s} to be rejected");
3210 }
3211 }
3212
3213 #[test]
3214 fn test_background_task_id_rejects_invalid_hex() {
3215 assert!(BackgroundTaskID::from_str("bt-ggg").is_err());
3216 }
3217
3218 #[test]
3219 fn test_background_task_id_converts_to_task_id() {
3220 let bt = BackgroundTaskID::from_str("bt-55b5").unwrap();
3223 let task: TaskID = bt.into();
3224 assert_eq!(task.value(), bt.value());
3225 assert_eq!(task.to_string(), "task-55b5");
3226 }
3227
3228 #[test]
3229 fn test_task_id_converts_to_background_task_id() {
3230 let task = TaskID::from_str("task-abc123").unwrap();
3231 let bt: BackgroundTaskID = task.into();
3232 assert_eq!(bt.value(), task.value());
3233 assert_eq!(bt.to_string(), "bt-abc123");
3234 }
3235
3236 #[test]
3237 fn test_background_task_id_conversion_is_lossless() {
3238 for raw in [0u64, 1, 0x55b5, u64::MAX] {
3239 let bt = BackgroundTaskID::from(raw);
3240 let round: BackgroundTaskID = TaskID::from(bt).into();
3241 assert_eq!(round.value(), raw);
3242 }
3243 }
3244
3245 #[test]
3247 fn test_task_id_from_u64() {
3248 let id = TaskID::from(555666);
3249 assert_eq!(id.value(), 555666);
3250 }
3251
3252 #[test]
3253 fn test_task_id_display() {
3254 let id = TaskID::from(0x123456);
3255 assert_eq!(format!("{}", id), "task-123456");
3256 }
3257
3258 #[test]
3259 fn test_task_id_from_str_valid() {
3260 let id = TaskID::from_str("task-123456").unwrap();
3261 assert_eq!(id.value(), 0x123456);
3262 }
3263
3264 #[test]
3265 fn test_task_id_try_from_str_valid() {
3266 let id = TaskID::try_from("task-abcdef").unwrap();
3267 assert_eq!(id.value(), 0xabcdef);
3268 }
3269
3270 #[test]
3271 fn test_task_id_try_from_string_valid() {
3272 let id = TaskID::try_from("task-fedcba".to_string()).unwrap();
3273 assert_eq!(id.value(), 0xfedcba);
3274 }
3275
3276 #[test]
3277 fn test_task_id_from_str_invalid_prefix() {
3278 let result = TaskID::from_str("t-123");
3279 assert!(result.is_err());
3280 match result {
3281 Err(Error::InvalidParameters(msg)) => {
3282 assert!(msg.contains("must start with 'task-'"));
3283 }
3284 _ => panic!("Expected InvalidParameters error"),
3285 }
3286 }
3287
3288 #[test]
3289 fn test_task_id_from_str_invalid_hex() {
3290 let result = TaskID::from_str("task-zzz");
3291 assert!(result.is_err());
3292 }
3293
3294 #[test]
3295 fn test_task_id_into_u64() {
3296 let id = TaskID::from(777888);
3297 let value: u64 = id.into();
3298 assert_eq!(value, 777888);
3299 }
3300
3301 #[test]
3303 fn test_dataset_id_from_u64() {
3304 let id = DatasetID::from(1193046);
3305 assert_eq!(id.value(), 1193046);
3306 }
3307
3308 #[test]
3309 fn test_dataset_id_display() {
3310 let id = DatasetID::from(0x123abc);
3311 assert_eq!(format!("{}", id), "ds-123abc");
3312 }
3313
3314 #[test]
3315 fn test_dataset_id_from_str_valid() {
3316 let id = DatasetID::from_str("ds-456def").unwrap();
3317 assert_eq!(id.value(), 0x456def);
3318 }
3319
3320 #[test]
3321 fn test_dataset_id_try_from_str_valid() {
3322 let id = DatasetID::try_from("ds-789abc").unwrap();
3323 assert_eq!(id.value(), 0x789abc);
3324 }
3325
3326 #[test]
3327 fn test_dataset_id_try_from_string_valid() {
3328 let id = DatasetID::try_from("ds-fedcba".to_string()).unwrap();
3329 assert_eq!(id.value(), 0xfedcba);
3330 }
3331
3332 #[test]
3333 fn test_dataset_id_from_str_invalid_prefix() {
3334 let result = DatasetID::from_str("dataset-123");
3335 assert!(result.is_err());
3336 match result {
3337 Err(Error::InvalidParameters(msg)) => {
3338 assert!(msg.contains("must start with 'ds-'"));
3339 }
3340 _ => panic!("Expected InvalidParameters error"),
3341 }
3342 }
3343
3344 #[test]
3345 fn test_dataset_id_from_str_invalid_hex() {
3346 let result = DatasetID::from_str("ds-zzz");
3347 assert!(result.is_err());
3348 }
3349
3350 #[test]
3351 fn test_dataset_id_into_u64() {
3352 let id = DatasetID::from(111111);
3353 let value: u64 = id.into();
3354 assert_eq!(value, 111111);
3355 }
3356
3357 #[test]
3358 fn dataset_id_default_is_zero() {
3359 assert_eq!(DatasetID::default().value(), 0);
3360 }
3361
3362 #[test]
3363 fn dataset_params_default_is_all_zero_and_empty() {
3364 let params = DatasetParams::default();
3365 assert_eq!(params.dataset_id().value(), 0);
3366 assert_eq!(params.annotation_set_id().value(), 0);
3367 assert_eq!(params.train_group(), "");
3368 assert_eq!(params.val_group(), "");
3369 }
3370
3371 #[test]
3373 fn test_annotation_set_id_from_u64() {
3374 let id = AnnotationSetID::from(222333);
3375 assert_eq!(id.value(), 222333);
3376 }
3377
3378 #[test]
3379 fn test_annotation_set_id_display() {
3380 let id = AnnotationSetID::from(0xabcdef);
3381 assert_eq!(format!("{}", id), "as-abcdef");
3382 }
3383
3384 #[test]
3385 fn test_annotation_set_id_from_str_valid() {
3386 let id = AnnotationSetID::from_str("as-abcdef").unwrap();
3387 assert_eq!(id.value(), 0xabcdef);
3388 }
3389
3390 #[test]
3391 fn test_annotation_set_id_try_from_str_valid() {
3392 let id = AnnotationSetID::try_from("as-123456").unwrap();
3393 assert_eq!(id.value(), 0x123456);
3394 }
3395
3396 #[test]
3397 fn test_annotation_set_id_try_from_string_valid() {
3398 let id = AnnotationSetID::try_from("as-fedcba".to_string()).unwrap();
3399 assert_eq!(id.value(), 0xfedcba);
3400 }
3401
3402 #[test]
3403 fn test_annotation_set_id_from_str_invalid_prefix() {
3404 let result = AnnotationSetID::from_str("annotation-123");
3405 assert!(result.is_err());
3406 match result {
3407 Err(Error::InvalidParameters(msg)) => {
3408 assert!(msg.contains("must start with 'as-'"));
3409 }
3410 _ => panic!("Expected InvalidParameters error"),
3411 }
3412 }
3413
3414 #[test]
3415 fn test_annotation_set_id_from_str_invalid_hex() {
3416 let result = AnnotationSetID::from_str("as-zzz");
3417 assert!(result.is_err());
3418 }
3419
3420 #[test]
3421 fn test_annotation_set_id_into_u64() {
3422 let id = AnnotationSetID::from(444555);
3423 let value: u64 = id.into();
3424 assert_eq!(value, 444555);
3425 }
3426
3427 #[test]
3429 fn test_sample_id_from_u64() {
3430 let id = SampleID::from(666777);
3431 assert_eq!(id.value(), 666777);
3432 }
3433
3434 #[test]
3435 fn test_sample_id_display() {
3436 let id = SampleID::from(0x987654);
3437 assert_eq!(format!("{}", id), "s-987654");
3438 }
3439
3440 #[test]
3441 fn test_sample_id_try_from_str_valid() {
3442 let id = SampleID::try_from("s-987654").unwrap();
3443 assert_eq!(id.value(), 0x987654);
3444 }
3445
3446 #[test]
3447 fn test_sample_id_try_from_str_invalid_prefix() {
3448 let result = SampleID::try_from("sample-123");
3449 assert!(result.is_err());
3450 match result {
3451 Err(Error::InvalidParameters(msg)) => {
3452 assert!(msg.contains("must start with 's-'"));
3453 }
3454 _ => panic!("Expected InvalidParameters error"),
3455 }
3456 }
3457
3458 #[test]
3459 fn test_sample_id_try_from_str_invalid_hex() {
3460 let result = SampleID::try_from("s-zzz");
3461 assert!(result.is_err());
3462 }
3463
3464 #[test]
3465 fn test_sample_id_into_u64() {
3466 let id = SampleID::from(888999);
3467 let value: u64 = id.into();
3468 assert_eq!(value, 888999);
3469 }
3470
3471 #[test]
3473 fn test_app_id_from_u64() {
3474 let id = AppId::from(123123);
3475 assert_eq!(id.value(), 123123);
3476 }
3477
3478 #[test]
3479 fn test_app_id_display() {
3480 let id = AppId::from(0x456789);
3481 assert_eq!(format!("{}", id), "app-456789");
3482 }
3483
3484 #[test]
3485 fn test_app_id_try_from_str_valid() {
3486 let id = AppId::try_from("app-456789").unwrap();
3487 assert_eq!(id.value(), 0x456789);
3488 }
3489
3490 #[test]
3491 fn test_app_id_try_from_str_invalid_prefix() {
3492 let result = AppId::try_from("application-123");
3493 assert!(result.is_err());
3494 match result {
3495 Err(Error::InvalidParameters(msg)) => {
3496 assert!(msg.contains("must start with 'app-'"));
3497 }
3498 _ => panic!("Expected InvalidParameters error"),
3499 }
3500 }
3501
3502 #[test]
3503 fn test_app_id_try_from_str_invalid_hex() {
3504 let result = AppId::try_from("app-zzz");
3505 assert!(result.is_err());
3506 }
3507
3508 #[test]
3509 fn test_app_id_into_u64() {
3510 let id = AppId::from(321321);
3511 let value: u64 = id.into();
3512 assert_eq!(value, 321321);
3513 }
3514
3515 #[test]
3517 fn test_image_id_from_u64() {
3518 let id = ImageId::from(789789);
3519 assert_eq!(id.value(), 789789);
3520 }
3521
3522 #[test]
3523 fn test_image_id_display() {
3524 let id = ImageId::from(0xabcd1234);
3525 assert_eq!(format!("{}", id), "im-abcd1234");
3526 }
3527
3528 #[test]
3529 fn test_image_id_try_from_str_valid() {
3530 let id = ImageId::try_from("im-abcd1234").unwrap();
3531 assert_eq!(id.value(), 0xabcd1234);
3532 }
3533
3534 #[test]
3535 fn test_image_id_try_from_str_invalid_prefix() {
3536 let result = ImageId::try_from("image-123");
3537 assert!(result.is_err());
3538 match result {
3539 Err(Error::InvalidParameters(msg)) => {
3540 assert!(msg.contains("must start with 'im-'"));
3541 }
3542 _ => panic!("Expected InvalidParameters error"),
3543 }
3544 }
3545
3546 #[test]
3547 fn test_image_id_try_from_str_invalid_hex() {
3548 let result = ImageId::try_from("im-zzz");
3549 assert!(result.is_err());
3550 }
3551
3552 #[test]
3553 fn test_image_id_into_u64() {
3554 let id = ImageId::from(987987);
3555 let value: u64 = id.into();
3556 assert_eq!(value, 987987);
3557 }
3558
3559 #[test]
3561 fn test_id_types_equality() {
3562 let id1 = ProjectID::from(12345);
3563 let id2 = ProjectID::from(12345);
3564 let id3 = ProjectID::from(54321);
3565
3566 assert_eq!(id1, id2);
3567 assert_ne!(id1, id3);
3568 }
3569
3570 #[test]
3571 fn test_id_types_hash() {
3572 use std::collections::HashSet;
3573
3574 let mut set = HashSet::new();
3575 set.insert(DatasetID::from(100));
3576 set.insert(DatasetID::from(200));
3577 set.insert(DatasetID::from(100)); assert_eq!(set.len(), 2);
3580 assert!(set.contains(&DatasetID::from(100)));
3581 assert!(set.contains(&DatasetID::from(200)));
3582 }
3583
3584 #[test]
3585 fn test_id_types_copy_clone() {
3586 let id1 = ExperimentID::from(999);
3587 let id2 = id1; let id3 = id1; assert_eq!(id1, id2);
3591 assert_eq!(id1, id3);
3592 }
3593
3594 #[test]
3596 fn test_id_zero_value() {
3597 let id = ProjectID::from(0);
3598 assert_eq!(format!("{}", id), "p-0");
3599 assert_eq!(id.value(), 0);
3600 }
3601
3602 #[test]
3603 fn test_id_max_value() {
3604 let id = ProjectID::from(u64::MAX);
3605 assert_eq!(format!("{}", id), "p-ffffffffffffffff");
3606 assert_eq!(id.value(), u64::MAX);
3607 }
3608
3609 #[test]
3610 fn test_id_round_trip_conversion() {
3611 let original = 0xdeadbeef_u64;
3612 let id = TrainingSessionID::from(original);
3613 let back: u64 = id.into();
3614 assert_eq!(original, back);
3615 }
3616
3617 #[test]
3618 fn test_id_case_insensitive_hex() {
3619 let id1 = DatasetID::from_str("ds-ABCDEF").unwrap();
3621 let id2 = DatasetID::from_str("ds-abcdef").unwrap();
3622 assert_eq!(id1.value(), id2.value());
3623 }
3624
3625 #[test]
3626 fn test_id_with_leading_zeros() {
3627 let id = ProjectID::from_str("p-00001234").unwrap();
3628 assert_eq!(id.value(), 0x1234);
3629 }
3630
3631 #[test]
3633 fn test_parameter_integer() {
3634 let param = Parameter::Integer(42);
3635 match param {
3636 Parameter::Integer(val) => assert_eq!(val, 42),
3637 _ => panic!("Expected Integer variant"),
3638 }
3639 }
3640
3641 #[test]
3642 fn test_parameter_real() {
3643 let param = Parameter::Real(2.5);
3644 match param {
3645 Parameter::Real(val) => assert_eq!(val, 2.5),
3646 _ => panic!("Expected Real variant"),
3647 }
3648 }
3649
3650 #[test]
3651 fn test_parameter_boolean() {
3652 let param = Parameter::Boolean(true);
3653 match param {
3654 Parameter::Boolean(val) => assert!(val),
3655 _ => panic!("Expected Boolean variant"),
3656 }
3657 }
3658
3659 #[test]
3660 fn test_parameter_string() {
3661 let param = Parameter::String("test".to_string());
3662 match param {
3663 Parameter::String(val) => assert_eq!(val, "test"),
3664 _ => panic!("Expected String variant"),
3665 }
3666 }
3667
3668 #[test]
3669 fn test_parameter_array() {
3670 let param = Parameter::Array(vec![
3671 Parameter::Integer(1),
3672 Parameter::Integer(2),
3673 Parameter::Integer(3),
3674 ]);
3675 match param {
3676 Parameter::Array(arr) => assert_eq!(arr.len(), 3),
3677 _ => panic!("Expected Array variant"),
3678 }
3679 }
3680
3681 #[test]
3682 fn test_parameter_object() {
3683 let mut map = HashMap::new();
3684 map.insert("key".to_string(), Parameter::Integer(100));
3685 let param = Parameter::Object(map);
3686 match param {
3687 Parameter::Object(obj) => {
3688 assert_eq!(obj.len(), 1);
3689 assert!(obj.contains_key("key"));
3690 }
3691 _ => panic!("Expected Object variant"),
3692 }
3693 }
3694
3695 #[test]
3696 fn test_parameter_clone() {
3697 let param1 = Parameter::Integer(42);
3698 let param2 = param1.clone();
3699 assert_eq!(param1, param2);
3700 }
3701
3702 #[test]
3703 fn test_parameter_nested() {
3704 let inner_array = Parameter::Array(vec![Parameter::Integer(1), Parameter::Integer(2)]);
3705 let outer_array = Parameter::Array(vec![inner_array.clone(), inner_array]);
3706
3707 match outer_array {
3708 Parameter::Array(arr) => {
3709 assert_eq!(arr.len(), 2);
3710 }
3711 _ => panic!("Expected Array variant"),
3712 }
3713 }
3714
3715 macro_rules! test_typeid_conversions {
3718 ($test_name:ident, $type:ty, $prefix:literal, $wrong_prefix:literal) => {
3719 #[test]
3720 fn $test_name() {
3721 let id = <$type>::from(0xabc123);
3723 assert_eq!(id.value(), 0xabc123);
3724
3725 assert_eq!(format!("{}", id), concat!($prefix, "-abc123"));
3727
3728 let id: $type = concat!($prefix, "-abc123").parse().unwrap();
3730 assert_eq!(id.value(), 0xabc123);
3731
3732 assert!(concat!($wrong_prefix, "-abc").parse::<$type>().is_err());
3734
3735 assert!("abc123".parse::<$type>().is_err());
3737
3738 assert!(concat!($prefix, "-xyz").parse::<$type>().is_err());
3740
3741 let id = <$type>::try_from(concat!($prefix, "-abc123")).unwrap();
3743 assert_eq!(id.value(), 0xabc123);
3744
3745 let id = <$type>::try_from(concat!($prefix, "-abc123").to_string()).unwrap();
3747 assert_eq!(id.value(), 0xabc123);
3748
3749 let id = <$type>::from(0xabc123);
3751 let json = serde_json::to_string(&id).unwrap();
3752 let parsed: $type = serde_json::from_str(&json).unwrap();
3753 assert_eq!(id, parsed);
3754
3755 let id = <$type>::from(0xabc123);
3757 let val: u64 = id.into();
3758 assert_eq!(val, 0xabc123);
3759 }
3760 };
3761 }
3762
3763 test_typeid_conversions!(test_organization_id_conversions, OrganizationID, "org", "p");
3764 test_typeid_conversions!(test_project_id_conversions, ProjectID, "p", "org");
3765 test_typeid_conversions!(test_experiment_id_conversions, ExperimentID, "exp", "p");
3766 test_typeid_conversions!(
3767 test_training_session_id_conversions,
3768 TrainingSessionID,
3769 "t",
3770 "v"
3771 );
3772 test_typeid_conversions!(
3773 test_validation_session_id_conversions,
3774 ValidationSessionID,
3775 "v",
3776 "t"
3777 );
3778 test_typeid_conversions!(test_snapshot_id_conversions, SnapshotID, "ss", "ds");
3779 test_typeid_conversions!(test_task_id_conversions, TaskID, "task", "t");
3780 test_typeid_conversions!(test_dataset_id_conversions, DatasetID, "ds", "ss");
3781 test_typeid_conversions!(
3782 test_annotation_set_id_conversions,
3783 AnnotationSetID,
3784 "as",
3785 "ds"
3786 );
3787 test_typeid_conversions!(test_sample_id_conversions, SampleID, "s", "p");
3788 test_typeid_conversions!(test_app_id_conversions, AppId, "app", "p");
3789 test_typeid_conversions!(test_image_id_conversions, ImageId, "im", "se");
3790 test_typeid_conversions!(test_sequence_id_conversions, SequenceId, "se", "im");
3791
3792 #[test]
3795 fn test_version_tag_deserialize_full() {
3796 let json = r#"{
3797 "id": 456, "dataset_id": 1715004, "name": "training-v1.0",
3798 "serial": 42, "description": "Ready for production",
3799 "created_by": "user@example.com", "created_at": "2025-01-15T10:30:00Z",
3800 "image_count": 50000, "annotation_counts": {"box": 150000, "seg": 20000},
3801 "sensor_counts": {"lidar": 25000}, "label_count": 15,
3802 "annotation_set_count": 3, "snapshot_id": 789
3803 }"#;
3804 let tag: VersionTag = serde_json::from_str(json).unwrap();
3805 assert_eq!(tag.name(), "training-v1.0");
3806 assert_eq!(tag.serial(), 42);
3807 assert_eq!(tag.image_count(), 50000);
3808 assert_eq!(tag.annotation_counts().get("box"), Some(&150000));
3809 assert_eq!(tag.snapshot_id(), Some(789));
3810 }
3811
3812 #[test]
3813 fn test_version_tag_deserialize_omitempty() {
3814 let json = r#"{
3816 "id": 1, "dataset_id": 2, "name": "v1.0", "serial": 5,
3817 "description": "", "created_by": "user",
3818 "created_at": "2025-01-01T00:00:00Z"
3819 }"#;
3820 let tag: VersionTag = serde_json::from_str(json).unwrap();
3821 assert_eq!(tag.snapshot_id(), None);
3822 assert_eq!(tag.image_count(), 0);
3823 assert!(tag.annotation_counts().is_empty());
3824 }
3825
3826 #[test]
3827 fn test_changelog_entry_deserialize_omitempty() {
3828 let json = r#"{
3830 "id": 1, "dataset_id": 2, "serial": 3, "entity_type": "image",
3831 "operation": "bulk_create", "change_data": {"count": 5},
3832 "username": "user", "organization_id": 1,
3833 "created_at": "2025-01-01T00:00:00Z", "message": ""
3834 }"#;
3835 let entry: ChangelogEntry = serde_json::from_str(json).unwrap();
3836 assert!(entry.entity_id().is_none());
3837 assert!(entry.s3_version_ids().is_empty());
3838 assert_eq!(entry.entity_type(), "image");
3839 assert_eq!(entry.operation(), "bulk_create");
3840 }
3841
3842 #[test]
3843 fn test_changelog_response_deserialize() {
3844 let json = r#"{
3845 "entries": [], "count": 0, "continue_token": ""
3846 }"#;
3847 let resp: ChangelogResponse = serde_json::from_str(json).unwrap();
3848 assert!(resp.entries.is_empty());
3849 assert_eq!(resp.count, 0);
3850 assert!(resp.continue_token.is_empty());
3851 assert!(resp.from_serial.is_none());
3852 }
3853
3854 #[test]
3855 fn test_version_current_no_latest_tag() {
3856 let json = r#"{
3858 "dataset_id": 100, "current_serial": 5, "tags": []
3859 }"#;
3860 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3861 assert!(resp.latest_tag.is_none());
3862 assert!(resp.tags.is_empty());
3863 assert_eq!(resp.current_serial, 5);
3864 }
3865
3866 #[test]
3867 fn test_version_current_with_latest_tag() {
3868 let json = r#"{
3869 "dataset_id": 100, "current_serial": 42,
3870 "latest_tag": {
3871 "id": 1, "dataset_id": 100, "name": "v1.0", "serial": 42,
3872 "description": "test", "created_by": "user",
3873 "created_at": "2025-01-01T00:00:00Z",
3874 "image_count": 10, "label_count": 2, "annotation_set_count": 1
3875 },
3876 "tags": []
3877 }"#;
3878 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3879 assert!(resp.latest_tag.is_some());
3880 assert_eq!(resp.latest_tag.unwrap().name(), "v1.0");
3881 }
3882
3883 #[test]
3884 fn test_version_tag_is_current_field() {
3885 let json = r#"{
3886 "id": 1, "dataset_id": 5, "name": "v1.0", "serial": 10,
3887 "created_by": "alice", "created_at": "2026-01-01T00:00:00Z",
3888 "is_current": true
3889 }"#;
3890 let tag: VersionTag = serde_json::from_str(json).unwrap();
3891 assert!(tag.is_current());
3892 }
3893
3894 #[test]
3895 fn test_version_tag_is_current_false() {
3896 let json = r#"{
3897 "id": 1, "dataset_id": 5, "name": "v1.0", "serial": 10,
3898 "created_by": "alice", "created_at": "2026-01-01T00:00:00Z",
3899 "is_current": false
3900 }"#;
3901 let tag: VersionTag = serde_json::from_str(json).unwrap();
3902 assert!(!tag.is_current());
3903 }
3904
3905 #[test]
3906 fn test_dataset_summary_deserialize() {
3907 let json = r#"{
3908 "dataset_id": 100, "current_serial": 10,
3909 "image_count": 5000, "annotation_counts": {"box": 10000},
3910 "sensor_counts": {}, "label_count": 8,
3911 "annotation_set_count": 2, "last_updated": "2025-06-01T12:00:00Z"
3912 }"#;
3913 let summary: DatasetSummary = serde_json::from_str(json).unwrap();
3914 assert_eq!(summary.image_count(), 5000);
3915 assert_eq!(summary.label_count(), 8);
3916 assert_eq!(summary.annotation_counts().get("box"), Some(&10000));
3917 }
3918
3919 #[test]
3920 fn test_restore_result_deserialize() {
3921 let json = r#"{
3922 "success": true, "new_serial": 45,
3923 "restored_from": {"tag": "v1.0", "serial": 42},
3924 "restored_counts": {"images": 5000, "labels": 15, "annotation_sets": 3},
3925 "message": "Dataset restored to tag v1.0"
3926 }"#;
3927 let result: RestoreResult = serde_json::from_str(json).unwrap();
3928 assert!(result.success);
3929 assert_eq!(result.new_serial, 45);
3930 assert_eq!(result.restored_from.tag, "v1.0");
3931 assert_eq!(result.restored_from.serial, 42);
3932 assert_eq!(result.restored_counts.images, 5000);
3933 }
3934
3935 #[test]
3936 fn test_sample_delete_params_serializes_all_fields() {
3937 let params = SampleDeleteParams {
3943 dataset_id: 42,
3944 image_ids: vec![1, 2, 3],
3945 sequence_ids: Vec::new(),
3946 delete_all: false,
3947 };
3948 let value = serde_json::to_value(¶ms).unwrap();
3949 let obj = value.as_object().unwrap();
3950 assert_eq!(obj.len(), 4);
3951 assert_eq!(obj["dataset_id"], serde_json::json!(42));
3952 assert_eq!(obj["image_ids"], serde_json::json!([1, 2, 3]));
3953 assert_eq!(obj["sequence_ids"], serde_json::json!([]));
3954 assert_eq!(obj["delete_all"], serde_json::json!(false));
3955 }
3956}
3957
3958#[cfg(test)]
3959mod tests_task_data_list {
3960 use super::*;
3961
3962 #[test]
3963 fn task_data_list_deserializes_from_server_shape() {
3964 let json = r#"{
3965 "server": "test.edgefirst.studio",
3966 "organization_uid": "org-abc123",
3967 "traces": ["trace/imx95.json"],
3968 "data": {
3969 "predictions": ["predictions.parquet"],
3970 "trace": ["imx95.json"]
3971 }
3972 }"#;
3973 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
3974 assert_eq!(parsed.server, "test.edgefirst.studio");
3975 assert_eq!(parsed.organization_uid, "org-abc123");
3976 assert_eq!(parsed.traces, vec!["trace/imx95.json"]);
3977 assert_eq!(
3978 parsed.data.get("predictions").unwrap(),
3979 &vec!["predictions.parquet".to_string()]
3980 );
3981 }
3982}
3983
3984#[cfg(test)]
3985mod tests_upload_data {
3986 #[test]
3990 fn folder_empty_string_is_normalised() {
3991 let folder: Option<&str> = Some("");
3992 assert!(folder.filter(|s| !s.is_empty()).is_none());
3993
3994 let folder_real: Option<&str> = Some("predictions");
3995 assert!(folder_real.filter(|s| !s.is_empty()).is_some());
3996 }
3997}
3998
3999#[cfg(test)]
4000mod tests_job_struct {
4001 use super::*;
4002
4003 #[test]
4004 fn job_deserializes_with_all_fields() {
4005 let json = r#"{
4006 "code": "edgefirst-validator:2.9.5",
4007 "title": "EdgeFirst Validator",
4008 "job_name": "smoke-test",
4009 "job_id": "aws-batch-abc",
4010 "state": "RUNNING",
4011 "launch": "2026-05-14T15:00:00Z",
4012 "task_id": 6789
4013 }"#;
4014 let job: Job = serde_json::from_str(json).unwrap();
4015 assert_eq!(job.code, "edgefirst-validator:2.9.5");
4016 assert_eq!(job.title, "EdgeFirst Validator");
4017 assert_eq!(job.job_name, "smoke-test");
4018 assert_eq!(job.job_id, "aws-batch-abc");
4019 assert_eq!(job.state, "RUNNING");
4020 assert!(job.launch.is_some());
4021 assert_eq!(job.task_id, 6789);
4022 }
4023
4024 #[test]
4025 fn job_tolerates_missing_optional_fields() {
4026 let json = r#"{ "task_id": 42 }"#;
4030 let job: Job = serde_json::from_str(json).unwrap();
4031 assert_eq!(job.task_id, 42);
4032 assert!(job.code.is_empty());
4033 assert!(job.title.is_empty());
4034 assert!(job.job_name.is_empty());
4035 assert!(job.job_id.is_empty());
4036 assert!(job.state.is_empty());
4037 assert!(job.launch.is_none());
4038 }
4039
4040 #[test]
4041 fn job_task_id_accessor_saturates_negative_to_zero() {
4042 let job = Job {
4047 code: String::new(),
4048 title: String::new(),
4049 job_name: String::new(),
4050 job_id: String::new(),
4051 state: String::new(),
4052 launch: None,
4053 task_id: -1,
4054 };
4055 assert_eq!(job.task_id().value(), 0);
4056 }
4057
4058 #[test]
4059 fn job_task_id_accessor_passes_through_positive_values() {
4060 let job = Job {
4061 code: String::new(),
4062 title: String::new(),
4063 job_name: String::new(),
4064 job_id: String::new(),
4065 state: String::new(),
4066 launch: None,
4067 task_id: 12345,
4068 };
4069 assert_eq!(job.task_id().value(), 12345);
4070 }
4071
4072 #[test]
4073 fn job_ignores_unknown_fields() {
4074 let json = r#"{
4078 "code": "x",
4079 "task_id": 1,
4080 "docker_task": { "image": "x" },
4081 "aws_region": "us-east-1",
4082 "tags": ["a", "b"]
4083 }"#;
4084 let job: Job = serde_json::from_str(json).unwrap();
4085 assert_eq!(job.task_id, 1);
4086 }
4087}
4088
4089#[cfg(test)]
4090mod tests_task_info_schema_tolerance {
4091 use super::*;
4092
4093 #[test]
4098 fn task_info_accepts_task_description_field() {
4099 let json = r#"{
4101 "id": 6699,
4102 "type": "edgefirst-validator:2.9.5",
4103 "task_description": "Profiler run for IMX95",
4104 "status": "running"
4105 }"#;
4106 let info: TaskInfo = serde_json::from_str(json).unwrap();
4107 assert_eq!(info.description(), "Profiler run for IMX95");
4108 }
4109
4110 #[test]
4111 fn task_info_accepts_legacy_description_field() {
4112 let json = r#"{
4114 "id": 6699,
4115 "type": "edgefirst-validator:2.9.5",
4116 "description": "Legacy description"
4117 }"#;
4118 let info: TaskInfo = serde_json::from_str(json).unwrap();
4119 assert_eq!(info.description(), "Legacy description");
4120 }
4121
4122 #[test]
4123 fn task_info_tolerates_missing_description() {
4124 let json = r#"{
4126 "id": 6699,
4127 "type": "x"
4128 }"#;
4129 let info: TaskInfo = serde_json::from_str(json).unwrap();
4130 assert!(info.description().is_empty());
4131 }
4132
4133 #[test]
4134 fn task_info_tolerates_missing_dates_via_default() {
4135 let json = r#"{
4137 "id": 6699,
4138 "type": "x"
4139 }"#;
4140 let info: TaskInfo = serde_json::from_str(json).unwrap();
4141 assert_eq!(info.id().value(), 6699);
4143 }
4144
4145 #[test]
4146 fn task_info_status_accessor_returns_option() {
4147 let json = r#"{
4148 "id": 1,
4149 "type": "x"
4150 }"#;
4151 let info: TaskInfo = serde_json::from_str(json).unwrap();
4152 assert!(info.status().is_none());
4153 }
4154
4155 #[test]
4156 fn task_info_stages_returns_empty_map_when_unset() {
4157 let json = r#"{
4158 "id": 1,
4159 "type": "x"
4160 }"#;
4161 let info: TaskInfo = serde_json::from_str(json).unwrap();
4162 let stages = info.stages();
4163 assert!(stages.is_empty());
4164 }
4165}
4166
4167#[cfg(test)]
4168mod tests_stage_struct {
4169 use super::*;
4170
4171 #[test]
4172 fn stage_new_sets_only_supplied_fields() {
4173 let stage = Stage::new(
4174 None,
4175 "download".into(),
4176 Some("running".into()),
4177 Some("fetching".into()),
4178 42,
4179 );
4180 assert!(stage.task_id().is_none());
4181 assert_eq!(stage.stage(), "download");
4182 assert_eq!(stage.status().as_deref(), Some("running"));
4183 assert_eq!(stage.message().as_deref(), Some("fetching"));
4184 assert_eq!(stage.percentage(), 42);
4185 assert!(stage.description().is_none());
4187 }
4188
4189 #[test]
4190 fn stage_serializes_without_optional_none_fields() {
4191 let stage = Stage::new(None, "init".into(), None, None, 0);
4193 let json = serde_json::to_value(&stage).unwrap();
4194 assert!(json.get("status").is_none(), "got: {json}");
4195 assert!(json.get("message").is_none(), "got: {json}");
4196 assert!(json.get("docker_task_id").is_none(), "got: {json}");
4197 assert_eq!(json["stage"], "init");
4199 assert_eq!(json["percentage"], 0);
4200 }
4201
4202 #[test]
4203 fn stage_serializes_task_id_when_present() {
4204 let task_id = TaskID::from(0xdeadu64);
4205 let stage = Stage::new(Some(task_id), "x".into(), None, None, 0);
4206 let json = serde_json::to_value(&stage).unwrap();
4207 assert!(json.get("docker_task_id").is_some());
4210 }
4211
4212 #[test]
4213 fn stage_round_trips_through_json() {
4214 let stage = Stage::new(
4215 None,
4216 "train".into(),
4217 Some("done".into()),
4218 Some("epoch 100".into()),
4219 100,
4220 );
4221 let s = serde_json::to_string(&stage).unwrap();
4222 let back: Stage = serde_json::from_str(&s).unwrap();
4223 assert_eq!(back.stage(), "train");
4224 assert_eq!(back.status().as_deref(), Some("done"));
4225 assert_eq!(back.message().as_deref(), Some("epoch 100"));
4226 assert_eq!(back.percentage(), 100);
4227 }
4228}
4229
4230#[cfg(test)]
4231mod tests_task_data_list_extra {
4232 use super::*;
4233
4234 #[test]
4235 fn task_data_list_with_empty_data_map() {
4236 let json = r#"{
4237 "server": "studio",
4238 "organization_uid": "org-1",
4239 "traces": [],
4240 "data": {}
4241 }"#;
4242 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4243 assert!(parsed.traces.is_empty());
4244 assert!(parsed.data.is_empty());
4245 }
4246
4247 #[test]
4248 fn task_data_list_multiple_folders() {
4249 let json = r#"{
4250 "server": "studio",
4251 "organization_uid": "org-1",
4252 "traces": ["t1", "t2"],
4253 "data": {
4254 "predictions": ["a.parquet", "b.parquet"],
4255 "metrics": ["loss.json"]
4256 }
4257 }"#;
4258 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4259 assert_eq!(parsed.traces.len(), 2);
4260 assert_eq!(parsed.data.len(), 2);
4261 assert_eq!(parsed.data["predictions"].len(), 2);
4262 }
4263}
4264
4265#[cfg(test)]
4266mod tests_artifact_struct {
4267 use super::*;
4268
4269 #[test]
4270 fn artifact_accessors_return_strs() {
4271 let json = r#"{ "name": "best.onnx", "modelType": "yolo" }"#;
4274 let a: Artifact = serde_json::from_str(json).unwrap();
4275 assert_eq!(a.name(), "best.onnx");
4276 assert_eq!(a.model_type(), "yolo");
4277 }
4278}
4279
4280#[cfg(test)]
4281mod tests_task_status_serialize {
4282 use super::*;
4283
4284 #[test]
4285 fn task_status_uses_docker_task_id_wire_field() {
4286 let s = TaskStatus {
4287 task_id: TaskID::from(0x1a2bu64),
4288 status: "training".into(),
4289 };
4290 let json = serde_json::to_value(&s).unwrap();
4291 assert!(json.get("docker_task_id").is_some(), "got: {json}");
4293 assert_eq!(json["status"], "training");
4294 }
4295}
4296
4297#[cfg(test)]
4298mod tests_task_stages_serialize {
4299 use super::*;
4300
4301 #[test]
4302 fn task_stages_omits_empty_vec() {
4303 let stages = TaskStages {
4304 task_id: TaskID::from(1u64),
4305 stages: Vec::new(),
4306 };
4307 let json = serde_json::to_value(&stages).unwrap();
4308 assert!(json.get("stages").is_none(), "got: {json}");
4310 }
4311
4312 #[test]
4313 fn task_stages_serializes_non_empty_vec() {
4314 let stages = TaskStages {
4315 task_id: TaskID::from(1u64),
4316 stages: vec![std::collections::HashMap::from([(
4317 "stage".to_string(),
4318 "download".to_string(),
4319 )])],
4320 };
4321 let json = serde_json::to_value(&stages).unwrap();
4322 assert_eq!(json["stages"][0]["stage"], "download");
4323 }
4324}