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)]
869pub struct SnapshotFromDatasetResult {
870 #[serde(alias = "snapshot_id")]
872 pub id: SnapshotID,
873 #[serde(default)]
875 pub task_id: Option<TaskID>,
876 #[serde(default)]
880 pub cloud_instance_id: Option<String>,
881}
882
883#[derive(Deserialize)]
884pub struct Experiment {
885 id: ExperimentID,
886 project_id: ProjectID,
887 name: String,
888 description: String,
889}
890
891impl Display for Experiment {
892 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
893 write!(f, "{} {}", self.id, self.name)
894 }
895}
896
897impl Experiment {
898 pub fn id(&self) -> ExperimentID {
899 self.id
900 }
901
902 pub fn project_id(&self) -> ProjectID {
903 self.project_id
904 }
905
906 pub fn name(&self) -> &str {
907 &self.name
908 }
909
910 pub fn description(&self) -> &str {
911 &self.description
912 }
913
914 pub async fn project(&self, client: &client::Client) -> Result<Project, Error> {
915 client.project(self.project_id).await
916 }
917
918 pub async fn training_sessions(
919 &self,
920 client: &client::Client,
921 name: Option<&str>,
922 ) -> Result<Vec<TrainingSession>, Error> {
923 client.training_sessions(self.id, name).await
924 }
925}
926
927#[derive(Serialize, Debug)]
928pub struct PublishMetrics {
929 #[serde(rename = "trainer_session_id", skip_serializing_if = "Option::is_none")]
930 pub trainer_session_id: Option<TrainingSessionID>,
931 #[serde(
932 rename = "validate_session_id",
933 skip_serializing_if = "Option::is_none"
934 )]
935 pub validate_session_id: Option<ValidationSessionID>,
936 pub metrics: HashMap<String, Parameter>,
937}
938
939#[derive(Deserialize)]
940struct TrainingSessionParams {
941 #[serde(default)]
942 model_params: HashMap<String, Parameter>,
943 #[serde(default)]
944 dataset_params: DatasetParams,
945}
946
947#[derive(Deserialize)]
948pub struct TrainingSession {
949 id: TrainingSessionID,
950 #[serde(rename = "trainer_id")]
951 experiment_id: ExperimentID,
952 model: String,
953 name: String,
954 description: String,
955 params: TrainingSessionParams,
956 #[serde(rename = "docker_task")]
957 task: Task,
958}
959
960impl Display for TrainingSession {
961 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
962 write!(f, "{} {}", self.id, self.name())
963 }
964}
965
966impl TrainingSession {
967 pub fn id(&self) -> TrainingSessionID {
968 self.id
969 }
970
971 pub fn name(&self) -> &str {
972 &self.name
973 }
974
975 pub fn description(&self) -> &str {
976 &self.description
977 }
978
979 pub fn model(&self) -> &str {
980 &self.model
981 }
982
983 pub fn experiment_id(&self) -> ExperimentID {
984 self.experiment_id
985 }
986
987 pub fn task(&self) -> Task {
988 self.task.clone()
989 }
990
991 pub fn model_params(&self) -> &HashMap<String, Parameter> {
992 &self.params.model_params
993 }
994
995 pub fn dataset_params(&self) -> &DatasetParams {
996 &self.params.dataset_params
997 }
998
999 pub fn train_group(&self) -> &str {
1000 &self.params.dataset_params.train_group
1001 }
1002
1003 pub fn val_group(&self) -> &str {
1004 &self.params.dataset_params.val_group
1005 }
1006
1007 pub async fn experiment(&self, client: &client::Client) -> Result<Experiment, Error> {
1008 client.experiment(self.experiment_id).await
1009 }
1010
1011 pub async fn dataset(&self, client: &client::Client) -> Result<Dataset, Error> {
1012 if self.params.dataset_params.dataset_id.value() == 0 {
1013 return Err(Error::InvalidParameters(
1014 "training session has no dataset configured".into(),
1015 ));
1016 }
1017 client.dataset(self.params.dataset_params.dataset_id).await
1018 }
1019
1020 pub async fn annotation_set(&self, client: &client::Client) -> Result<AnnotationSet, Error> {
1021 if self.params.dataset_params.annotation_set_id.value() == 0 {
1022 return Err(Error::InvalidParameters(
1023 "training session has no annotation set configured".into(),
1024 ));
1025 }
1026 client
1027 .annotation_set(self.params.dataset_params.annotation_set_id)
1028 .await
1029 }
1030
1031 pub async fn artifacts(&self, client: &client::Client) -> Result<Vec<Artifact>, Error> {
1032 client.artifacts(self.id).await
1033 }
1034
1035 pub async fn metrics(
1036 &self,
1037 client: &client::Client,
1038 ) -> Result<HashMap<String, Parameter>, Error> {
1039 #[derive(Deserialize)]
1040 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1041 enum Response {
1042 Empty {},
1043 Map(HashMap<String, Parameter>),
1044 String(String),
1045 }
1046
1047 let params = HashMap::from([("trainer_session_id", self.id().value())]);
1048 let resp: Response = client
1049 .rpc("trainer.session.metrics".to_owned(), Some(params))
1050 .await?;
1051
1052 Ok(match resp {
1053 Response::String(metrics) => serde_json::from_str(&metrics)?,
1054 Response::Map(metrics) => metrics,
1055 Response::Empty {} => HashMap::new(),
1056 })
1057 }
1058
1059 pub async fn set_metrics(
1060 &self,
1061 client: &client::Client,
1062 metrics: HashMap<String, Parameter>,
1063 ) -> Result<(), Error> {
1064 let metrics = PublishMetrics {
1065 trainer_session_id: Some(self.id()),
1066 validate_session_id: None,
1067 metrics,
1068 };
1069
1070 let _: String = client
1071 .rpc("trainer.session.metrics".to_owned(), Some(metrics))
1072 .await?;
1073
1074 Ok(())
1075 }
1076
1077 pub async fn download_artifact(
1079 &self,
1080 client: &client::Client,
1081 filename: &str,
1082 ) -> Result<Vec<u8>, Error> {
1083 client
1084 .fetch(&format!(
1085 "download_model?training_session_id={}&file={}",
1086 self.id().value(),
1087 filename
1088 ))
1089 .await
1090 }
1091
1092 pub async fn upload_artifact(
1096 &self,
1097 client: &client::Client,
1098 filename: &str,
1099 path: PathBuf,
1100 ) -> Result<(), Error> {
1101 self.upload(client, &[(format!("artifacts/{}", filename), path)])
1102 .await
1103 }
1104
1105 pub async fn download_checkpoint(
1107 &self,
1108 client: &client::Client,
1109 filename: &str,
1110 ) -> Result<Vec<u8>, Error> {
1111 client
1112 .fetch(&format!(
1113 "download_checkpoint?folder=checkpoints&training_session_id={}&file={}",
1114 self.id().value(),
1115 filename
1116 ))
1117 .await
1118 }
1119
1120 pub async fn upload_checkpoint(
1124 &self,
1125 client: &client::Client,
1126 filename: &str,
1127 path: PathBuf,
1128 ) -> Result<(), Error> {
1129 self.upload(client, &[(format!("checkpoints/{}", filename), path)])
1130 .await
1131 }
1132
1133 pub async fn download(&self, client: &client::Client, filename: &str) -> Result<String, Error> {
1137 #[derive(Serialize)]
1138 struct DownloadRequest {
1139 session_id: TrainingSessionID,
1140 file_path: String,
1141 }
1142
1143 let params = DownloadRequest {
1144 session_id: self.id(),
1145 file_path: filename.to_string(),
1146 };
1147
1148 client
1149 .rpc("trainer.download.file".to_owned(), Some(params))
1150 .await
1151 }
1152
1153 pub async fn upload(
1154 &self,
1155 client: &client::Client,
1156 files: &[(String, PathBuf)],
1157 ) -> Result<(), Error> {
1158 let mut parts = Form::new().part(
1159 "params",
1160 Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1161 );
1162
1163 for (name, path) in files {
1164 let file_part = Part::file(path).await?.file_name(name.to_owned());
1165 parts = parts.part("file", file_part);
1166 }
1167
1168 let result = client.post_multipart("trainer.upload.files", parts).await?;
1169 trace!("TrainingSession::upload: {:?}", result);
1170 Ok(())
1171 }
1172}
1173
1174#[derive(Deserialize, Clone, Debug)]
1175pub struct ValidationSession {
1176 id: ValidationSessionID,
1177 description: String,
1178 dataset_id: DatasetID,
1179 experiment_id: ExperimentID,
1180 training_session_id: TrainingSessionID,
1181 #[serde(rename = "gt_annotation_set_id")]
1182 annotation_set_id: AnnotationSetID,
1183 #[serde(deserialize_with = "validation_session_params")]
1184 params: HashMap<String, Parameter>,
1185 #[serde(rename = "docker_task")]
1186 task: Task,
1187}
1188
1189fn validation_session_params<'de, D>(
1190 deserializer: D,
1191) -> Result<HashMap<String, Parameter>, D::Error>
1192where
1193 D: Deserializer<'de>,
1194{
1195 #[derive(Deserialize)]
1196 struct ModelParams {
1197 validation: Option<HashMap<String, Parameter>>,
1198 }
1199
1200 #[derive(Deserialize)]
1201 struct ValidateParams {
1202 model: String,
1203 }
1204
1205 #[derive(Deserialize)]
1206 struct Params {
1207 model_params: ModelParams,
1208 validate_params: ValidateParams,
1209 }
1210
1211 let params = Params::deserialize(deserializer)?;
1212 let params = match params.model_params.validation {
1213 Some(mut map) => {
1214 map.insert(
1215 "model".to_string(),
1216 Parameter::String(params.validate_params.model),
1217 );
1218 map
1219 }
1220 None => HashMap::from([(
1221 "model".to_string(),
1222 Parameter::String(params.validate_params.model),
1223 )]),
1224 };
1225
1226 Ok(params)
1227}
1228
1229impl ValidationSession {
1230 pub fn id(&self) -> ValidationSessionID {
1231 self.id
1232 }
1233
1234 pub fn name(&self) -> &str {
1235 self.task.name()
1236 }
1237
1238 pub fn description(&self) -> &str {
1239 &self.description
1240 }
1241
1242 pub fn dataset_id(&self) -> DatasetID {
1243 self.dataset_id
1244 }
1245
1246 pub fn experiment_id(&self) -> ExperimentID {
1247 self.experiment_id
1248 }
1249
1250 pub fn training_session_id(&self) -> TrainingSessionID {
1251 self.training_session_id
1252 }
1253
1254 pub fn annotation_set_id(&self) -> AnnotationSetID {
1255 self.annotation_set_id
1256 }
1257
1258 pub fn params(&self) -> &HashMap<String, Parameter> {
1259 &self.params
1260 }
1261
1262 pub fn task(&self) -> &Task {
1263 &self.task
1264 }
1265
1266 pub async fn metrics(
1267 &self,
1268 client: &client::Client,
1269 ) -> Result<HashMap<String, Parameter>, Error> {
1270 #[derive(Deserialize)]
1271 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1272 enum Response {
1273 Empty {},
1274 Map(HashMap<String, Parameter>),
1275 String(String),
1276 }
1277
1278 let params = HashMap::from([("validate_session_id", self.id().value())]);
1279 let resp: Response = client
1280 .rpc("validate.session.metrics".to_owned(), Some(params))
1281 .await?;
1282
1283 Ok(match resp {
1284 Response::String(metrics) => serde_json::from_str(&metrics)?,
1285 Response::Map(metrics) => metrics,
1286 Response::Empty {} => HashMap::new(),
1287 })
1288 }
1289
1290 pub async fn set_metrics(
1291 &self,
1292 client: &client::Client,
1293 metrics: HashMap<String, Parameter>,
1294 ) -> Result<(), Error> {
1295 let metrics = PublishMetrics {
1296 trainer_session_id: None,
1297 validate_session_id: Some(self.id()),
1298 metrics,
1299 };
1300
1301 let _: String = client
1302 .rpc("validate.session.metrics".to_owned(), Some(metrics))
1303 .await?;
1304
1305 Ok(())
1306 }
1307
1308 pub async fn upload_data(
1333 &self,
1334 client: &client::Client,
1335 files: &[(String, std::path::PathBuf)],
1336 folder: Option<&str>,
1337 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1338 ) -> Result<(), Error> {
1339 use futures::StreamExt;
1340 use std::sync::{
1341 Arc,
1342 atomic::{AtomicUsize, Ordering},
1343 };
1344 use tokio_util::io::ReaderStream;
1345
1346 let mut total: usize = 0;
1348 let mut file_meta = Vec::with_capacity(files.len());
1349 for (name, path) in files {
1350 let f = tokio::fs::File::open(path).await?;
1351 let len = f.metadata().await?.len() as usize;
1352 total += len;
1353 file_meta.push((name.clone(), f, len));
1354 }
1355
1356 let sent = Arc::new(AtomicUsize::new(0));
1358
1359 let mut form = Form::new().text("session_id", self.id().value().to_string());
1360 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
1361 form = form.text("folder", folder.to_owned());
1362 }
1363
1364 for (name, file, len) in file_meta {
1365 let reader_stream = ReaderStream::new(file);
1366 let sent_clone = sent.clone();
1367 let progress_clone = progress.clone();
1368 let progress_stream = reader_stream.inspect(move |chunk_result| {
1369 if let Ok(chunk) = chunk_result {
1370 let current =
1371 sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1372 if let Some(tx) = &progress_clone {
1377 let _ = tx.try_send(Progress {
1378 current,
1379 total,
1380 status: None,
1381 });
1382 }
1383 }
1384 });
1385 let body = reqwest::Body::wrap_stream(progress_stream);
1386 let part = Part::stream_with_length(body, len as u64).file_name(name);
1387 form = form.part("file", part);
1388 }
1389
1390 let result = match client.post_multipart("val.data.upload", form).await {
1391 Ok(_) => Ok(()),
1392 Err(Error::RpcError(code, msg)) => {
1393 Err(client::map_rpc_error("val.data.upload", code, msg, None))
1394 }
1395 Err(e) => Err(e),
1396 };
1397
1398 if result.is_ok()
1403 && let Some(tx) = progress
1404 {
1405 let _ = tx
1406 .send(Progress {
1407 current: total,
1408 total,
1409 status: None,
1410 })
1411 .await;
1412 }
1413 result
1414 }
1415
1416 pub async fn download_data(
1436 &self,
1437 client: &client::Client,
1438 filename: &str,
1439 output_path: &std::path::Path,
1440 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1441 ) -> Result<(), Error> {
1442 let req = client::ValDataDownloadRequest {
1443 session_id: self.id().value(),
1444 filename: filename.to_owned(),
1445 };
1446 match client
1447 .rpc_download("val.data.download", &req, output_path, progress)
1448 .await
1449 {
1450 Ok(()) => Ok(()),
1451 Err(Error::RpcError(code, msg)) => {
1452 Err(client::map_rpc_error("val.data.download", code, msg, None))
1453 }
1454 Err(e) => Err(e),
1455 }
1456 }
1457
1458 pub async fn data_list(&self, client: &client::Client) -> Result<Vec<String>, Error> {
1473 let req = client::ValDataListRequest {
1474 session_id: self.id().value(),
1475 };
1476 match client.rpc("val.data.list".to_owned(), Some(&req)).await {
1477 Ok(r) => Ok(r),
1478 Err(Error::RpcError(code, msg)) => {
1479 Err(client::map_rpc_error("val.data.list", code, msg, None))
1480 }
1481 Err(e) => Err(e),
1482 }
1483 }
1484}
1485
1486#[derive(Debug, Clone)]
1505pub struct StartValidationRequest {
1506 pub project_id: ProjectID,
1507 pub name: String,
1508 pub training_session_id: TrainingSessionID,
1509 pub model_file: String,
1510 pub val_type: String,
1511 pub params: HashMap<String, Parameter>,
1512 pub is_local: bool,
1513 pub is_kubernetes: bool,
1514 pub description: Option<String>,
1515 pub dataset_id: Option<DatasetID>,
1516 pub annotation_set_id: Option<AnnotationSetID>,
1517 pub snapshot_id: Option<SnapshotID>,
1518}
1519
1520#[derive(Deserialize, Debug, Clone)]
1535pub struct NewValidationSession {
1536 #[serde(rename = "id")]
1537 pub task_id: TaskID,
1538 #[serde(rename = "val_session_id", default)]
1539 pub session_id: Option<ValidationSessionID>,
1540}
1541
1542impl Display for NewValidationSession {
1543 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1544 match self.session_id {
1545 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1546 None => write!(f, "task {} (no session)", self.task_id),
1547 }
1548 }
1549}
1550
1551#[derive(Debug, Clone)]
1571pub struct StartTrainingRequest {
1572 pub project_id: ProjectID,
1574 pub name: String,
1576 pub experiment_id: ExperimentID,
1578 pub trainer_type: String,
1581 pub dataset_id: DatasetID,
1583 pub annotation_set_id: AnnotationSetID,
1585 pub tag_name: Option<String>,
1589 pub train_group: Option<String>,
1591 pub val_group: Option<String>,
1593 pub session_name: Option<String>,
1596 pub session_description: Option<String>,
1598 pub weights_session: Option<TrainingSessionID>,
1600 pub params: HashMap<String, Parameter>,
1602 pub is_local: bool,
1604 pub is_kubernetes: bool,
1606}
1607
1608#[derive(Deserialize, Debug, Clone)]
1621pub struct NewTrainingSession {
1622 #[serde(rename = "id")]
1623 pub task_id: TaskID,
1624 #[serde(rename = "train_session_id", default)]
1625 pub session_id: Option<TrainingSessionID>,
1626}
1627
1628impl Display for NewTrainingSession {
1629 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1630 match self.session_id {
1631 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1632 None => write!(f, "task {} (no session)", self.task_id),
1633 }
1634 }
1635}
1636
1637#[derive(Deserialize, Debug, Clone)]
1645pub struct Tag {
1646 pub id: u64,
1648 pub name: String,
1650 #[serde(default)]
1652 pub dataset_id: u64,
1653}
1654
1655#[derive(Deserialize, Clone, Debug, Default)]
1666#[serde(default)]
1667pub struct DatasetParams {
1668 dataset_id: DatasetID,
1669 annotation_set_id: AnnotationSetID,
1670 #[serde(rename = "train_group_name")]
1671 train_group: String,
1672 #[serde(rename = "val_group_name")]
1673 val_group: String,
1674}
1675
1676impl DatasetParams {
1677 pub fn dataset_id(&self) -> DatasetID {
1678 self.dataset_id
1679 }
1680
1681 pub fn annotation_set_id(&self) -> AnnotationSetID {
1682 self.annotation_set_id
1683 }
1684
1685 pub fn train_group(&self) -> &str {
1686 &self.train_group
1687 }
1688
1689 pub fn val_group(&self) -> &str {
1690 &self.val_group
1691 }
1692}
1693
1694#[derive(Serialize, Debug, Clone)]
1695pub struct TasksListParams {
1696 #[serde(skip_serializing_if = "Option::is_none")]
1697 pub continue_token: Option<String>,
1698 #[serde(skip_serializing_if = "Option::is_none")]
1699 pub types: Option<Vec<String>>,
1700 #[serde(rename = "manage_types", skip_serializing_if = "Option::is_none")]
1701 pub manager: Option<Vec<String>>,
1702 #[serde(skip_serializing_if = "Option::is_none")]
1703 pub status: Option<Vec<String>>,
1704}
1705
1706#[derive(Debug, Clone, Serialize, Deserialize)]
1712pub struct TaskDataList {
1713 pub server: String,
1714 #[serde(rename = "organization_uid")]
1715 pub organization_uid: String,
1716 #[serde(default)]
1717 pub traces: Vec<String>,
1718 #[serde(default)]
1719 pub data: std::collections::HashMap<String, Vec<String>>,
1720}
1721
1722#[derive(Debug, Clone, Serialize, Deserialize)]
1727pub struct Job {
1728 #[serde(default)]
1730 pub code: String,
1731 #[serde(default)]
1733 pub title: String,
1734 #[serde(default)]
1736 pub job_name: String,
1737 #[serde(default)]
1739 pub job_id: String,
1740 #[serde(default)]
1742 pub state: String,
1743 #[serde(default)]
1745 pub launch: Option<DateTime<Utc>>,
1746 pub task_id: i64,
1751}
1752
1753impl Job {
1754 pub fn task_id(&self) -> TaskID {
1760 TaskID::from(self.task_id.max(0) as u64)
1761 }
1762}
1763
1764#[derive(Deserialize, Debug, Clone)]
1765pub struct TasksListResult {
1766 pub tasks: Vec<Task>,
1767 pub continue_token: Option<String>,
1768}
1769
1770#[derive(Deserialize, Debug, Clone)]
1771pub struct Task {
1772 id: TaskID,
1773 name: String,
1774 #[serde(rename = "type")]
1775 workflow: String,
1776 status: String,
1777 #[serde(rename = "manage_type")]
1778 manager: Option<String>,
1779 #[serde(rename = "instance_type")]
1780 instance: String,
1781 #[serde(rename = "date")]
1782 created: DateTime<Utc>,
1783}
1784
1785impl Task {
1786 pub fn id(&self) -> TaskID {
1787 self.id
1788 }
1789
1790 pub fn name(&self) -> &str {
1791 &self.name
1792 }
1793
1794 pub fn workflow(&self) -> &str {
1795 &self.workflow
1796 }
1797
1798 pub fn status(&self) -> &str {
1799 &self.status
1800 }
1801
1802 pub fn manager(&self) -> Option<&str> {
1803 self.manager.as_deref()
1804 }
1805
1806 pub fn instance(&self) -> &str {
1807 &self.instance
1808 }
1809
1810 pub fn created(&self) -> &DateTime<Utc> {
1811 &self.created
1812 }
1813}
1814
1815impl Display for Task {
1816 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1817 write!(
1818 f,
1819 "{} [{:?} {}] {}",
1820 self.id,
1821 self.manager(),
1822 self.workflow(),
1823 self.name()
1824 )
1825 }
1826}
1827
1828#[derive(Deserialize, Debug, Clone)]
1829pub struct TaskInfo {
1830 id: TaskID,
1831 project_id: Option<ProjectID>,
1832 #[serde(rename = "task_description", alias = "description", default)]
1833 description: String,
1834 #[serde(rename = "type")]
1835 workflow: String,
1836 status: Option<String>,
1837 #[serde(default)]
1838 progress: TaskProgress,
1839 #[serde(
1840 rename = "created_date",
1841 alias = "created",
1842 default = "default_datetime_utc"
1843 )]
1844 created: DateTime<Utc>,
1845 #[serde(
1846 rename = "end_date",
1847 alias = "completed",
1848 default = "default_datetime_utc"
1849 )]
1850 completed: DateTime<Utc>,
1851}
1852
1853fn default_datetime_utc() -> DateTime<Utc> {
1854 DateTime::UNIX_EPOCH
1855}
1856
1857impl Display for TaskInfo {
1858 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1859 write!(f, "{} {}: {}", self.id, self.workflow(), self.description())
1860 }
1861}
1862
1863impl TaskInfo {
1864 pub fn id(&self) -> TaskID {
1865 self.id
1866 }
1867
1868 pub fn project_id(&self) -> Option<ProjectID> {
1869 self.project_id
1870 }
1871
1872 pub fn description(&self) -> &str {
1873 &self.description
1874 }
1875
1876 pub fn workflow(&self) -> &str {
1877 &self.workflow
1878 }
1879
1880 pub fn status(&self) -> &Option<String> {
1881 &self.status
1882 }
1883
1884 pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1885 let t = client.task_status(self.id(), status).await?;
1886 self.status = Some(t.status);
1887 Ok(())
1888 }
1889
1890 pub fn stages(&self) -> HashMap<String, Stage> {
1891 match &self.progress.stages {
1892 Some(stages) => stages.clone(),
1893 None => HashMap::new(),
1894 }
1895 }
1896
1897 pub async fn update_stage(
1898 &mut self,
1899 client: &Client,
1900 stage: &str,
1901 status: &str,
1902 message: &str,
1903 percentage: u8,
1904 ) -> Result<(), Error> {
1905 client
1906 .update_stage(self.id(), stage, status, message, percentage)
1907 .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 set_stages(
1914 &mut self,
1915 client: &Client,
1916 stages: &[(&str, &str)],
1917 ) -> Result<(), Error> {
1918 client.set_stages(self.id(), stages).await?;
1919 let t = client.task_info(self.id()).await?;
1920 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1921 Ok(())
1922 }
1923
1924 pub async fn data_list(&self, client: &client::Client) -> Result<TaskDataList, Error> {
1940 let req = client::TaskDataListRequest {
1941 task_id: self.id().value(),
1942 };
1943 match client.rpc("task.data.list".to_owned(), Some(&req)).await {
1944 Ok(r) => Ok(r),
1945 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1946 "task.data.list",
1947 code,
1948 msg,
1949 Some(self.id()),
1950 )),
1951 Err(e) => Err(e),
1952 }
1953 }
1954
1955 pub async fn upload_data(
1976 &self,
1977 client: &client::Client,
1978 path: &std::path::Path,
1979 folder: Option<&str>,
1980 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1981 ) -> Result<(), Error> {
1982 use futures::StreamExt;
1983 use std::sync::{
1984 Arc,
1985 atomic::{AtomicUsize, Ordering},
1986 };
1987 use tokio_util::io::ReaderStream;
1988
1989 let file_name = path
1990 .file_name()
1991 .and_then(|s| s.to_str())
1992 .ok_or_else(|| Error::InvalidParameters("path must have a UTF-8 filename".into()))?
1993 .to_owned();
1994
1995 let file = tokio::fs::File::open(path).await?;
1996 let total = file.metadata().await?.len() as usize;
1997 let sent = Arc::new(AtomicUsize::new(0));
1998
1999 let reader_stream = ReaderStream::new(file);
2000 let sent_clone = sent.clone();
2001 let progress_clone = progress.clone();
2002 let progress_stream = reader_stream.inspect(move |chunk_result| {
2003 if let Ok(chunk) = chunk_result {
2004 let current = sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
2005 if let Some(tx) = &progress_clone {
2011 let _ = tx.try_send(Progress {
2012 current,
2013 total,
2014 status: None,
2015 });
2016 }
2017 }
2018 });
2019
2020 let body = reqwest::Body::wrap_stream(progress_stream);
2021 let file_part = Part::stream_with_length(body, total as u64).file_name(file_name);
2022
2023 let mut form = Form::new().text("task_id", self.id().value().to_string());
2024 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
2025 form = form.text("folder", folder.to_owned());
2026 }
2027 form = form.part("file", file_part);
2028
2029 let result = match client.post_multipart("task.data.upload", form).await {
2030 Ok(_) => Ok(()),
2031 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2032 "task.data.upload",
2033 code,
2034 msg,
2035 Some(self.id()),
2036 )),
2037 Err(e) => Err(e),
2038 };
2039
2040 if result.is_ok()
2044 && let Some(tx) = progress
2045 {
2046 let _ = tx
2047 .send(Progress {
2048 current: total,
2049 total,
2050 status: None,
2051 })
2052 .await;
2053 }
2054 result
2055 }
2056
2057 pub async fn download_data(
2086 &self,
2087 client: &client::Client,
2088 file: &str,
2089 folder: Option<&str>,
2090 output_path: &std::path::Path,
2091 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
2092 ) -> Result<(), Error> {
2093 let folder = folder.unwrap_or("").to_owned();
2094 let req = client::TaskDataDownloadRequest {
2095 task_id: self.id().value(),
2096 folder,
2097 file: file.to_owned(),
2098 };
2099 match client
2100 .rpc_download("task.data.download", &req, output_path, progress)
2101 .await
2102 {
2103 Ok(()) => Ok(()),
2104 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2105 "task.data.download",
2106 code,
2107 msg,
2108 Some(self.id()),
2109 )),
2110 Err(e) => Err(e),
2111 }
2112 }
2113
2114 pub async fn add_chart(
2142 &self,
2143 client: &client::Client,
2144 group: &str,
2145 name: &str,
2146 data: Parameter,
2147 params: Option<Parameter>,
2148 ) -> Result<(), Error> {
2149 client::validate_chart_args(group, name)?;
2150 let req = client::TaskChartAddRequest {
2151 task_id: self.id().value(),
2152 group_name: group.to_owned(),
2153 chart_name: name.to_owned(),
2154 params,
2155 data,
2156 };
2157 let _resp: serde_json::Value =
2158 match client.rpc("task.chart.add".to_owned(), Some(&req)).await {
2159 Ok(r) => r,
2160 Err(Error::RpcError(code, msg)) => {
2161 return Err(client::map_rpc_error(
2162 "task.chart.add",
2163 code,
2164 msg,
2165 Some(self.id()),
2166 ));
2167 }
2168 Err(e) => return Err(e),
2169 };
2170 Ok(())
2171 }
2172
2173 pub async fn list_charts(
2190 &self,
2191 client: &client::Client,
2192 group: Option<&str>,
2193 ) -> Result<TaskDataList, Error> {
2194 let req = client::TaskChartListRequest {
2195 task_id: self.id().value(),
2196 group_name: group.unwrap_or("").to_owned(),
2197 };
2198 match client.rpc("task.chart.list".to_owned(), Some(&req)).await {
2199 Ok(r) => Ok(r),
2200 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2201 "task.chart.list",
2202 code,
2203 msg,
2204 Some(self.id()),
2205 )),
2206 Err(e) => Err(e),
2207 }
2208 }
2209
2210 pub async fn get_chart(
2229 &self,
2230 client: &client::Client,
2231 group: &str,
2232 name: &str,
2233 ) -> Result<Parameter, Error> {
2234 client::validate_chart_args(group, name)?;
2235 let req = client::TaskChartGetRequest {
2236 task_id: self.id().value(),
2237 group_name: group.to_owned(),
2238 chart_name: name.to_owned(),
2239 };
2240 match client.rpc("task.chart.get".to_owned(), Some(&req)).await {
2241 Ok(r) => Ok(r),
2242 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2243 "task.chart.get",
2244 code,
2245 msg,
2246 Some(self.id()),
2247 )),
2248 Err(e) => Err(e),
2249 }
2250 }
2251
2252 pub fn created(&self) -> &DateTime<Utc> {
2253 &self.created
2254 }
2255
2256 pub fn completed(&self) -> &DateTime<Utc> {
2257 &self.completed
2258 }
2259}
2260
2261#[derive(Deserialize, Debug, Default, Clone)]
2262pub struct TaskProgress {
2263 stages: Option<HashMap<String, Stage>>,
2264}
2265
2266#[derive(Serialize, Debug, Clone)]
2267pub struct TaskStatus {
2268 #[serde(rename = "docker_task_id")]
2269 pub task_id: TaskID,
2270 pub status: String,
2271}
2272
2273#[derive(Serialize, Deserialize, Debug, Clone)]
2274pub struct Stage {
2275 #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
2276 task_id: Option<TaskID>,
2277 stage: String,
2278 #[serde(skip_serializing_if = "Option::is_none")]
2279 status: Option<String>,
2280 #[serde(skip_serializing_if = "Option::is_none")]
2281 description: Option<String>,
2282 #[serde(skip_serializing_if = "Option::is_none")]
2283 message: Option<String>,
2284 percentage: u8,
2285}
2286
2287impl Stage {
2288 pub fn new(
2289 task_id: Option<TaskID>,
2290 stage: String,
2291 status: Option<String>,
2292 message: Option<String>,
2293 percentage: u8,
2294 ) -> Self {
2295 Stage {
2296 task_id,
2297 stage,
2298 status,
2299 description: None,
2300 message,
2301 percentage,
2302 }
2303 }
2304
2305 pub fn task_id(&self) -> &Option<TaskID> {
2306 &self.task_id
2307 }
2308
2309 pub fn stage(&self) -> &str {
2310 &self.stage
2311 }
2312
2313 pub fn status(&self) -> &Option<String> {
2314 &self.status
2315 }
2316
2317 pub fn description(&self) -> &Option<String> {
2318 &self.description
2319 }
2320
2321 pub fn message(&self) -> &Option<String> {
2322 &self.message
2323 }
2324
2325 pub fn percentage(&self) -> u8 {
2326 self.percentage
2327 }
2328}
2329
2330#[derive(Serialize, Debug)]
2331pub struct TaskStages {
2332 #[serde(rename = "docker_task_id")]
2333 pub task_id: TaskID,
2334 #[serde(skip_serializing_if = "Vec::is_empty")]
2335 pub stages: Vec<HashMap<String, String>>,
2336}
2337
2338#[derive(Deserialize, Debug)]
2339pub struct Artifact {
2340 name: String,
2341 #[serde(rename = "modelType")]
2342 model_type: String,
2343}
2344
2345impl Artifact {
2346 pub fn name(&self) -> &str {
2347 &self.name
2348 }
2349
2350 pub fn model_type(&self) -> &str {
2351 &self.model_type
2352 }
2353}
2354
2355#[derive(Deserialize, Serialize, Clone, Debug)]
2363pub struct VersionTag {
2364 id: u64,
2365 dataset_id: DatasetID,
2366 name: String,
2367 serial: u64,
2368 #[serde(default)]
2369 description: String,
2370 created_by: String,
2371 created_at: DateTime<Utc>,
2372 #[serde(default)]
2373 image_count: u64,
2374 #[serde(default)]
2375 annotation_counts: HashMap<String, u64>,
2376 #[serde(default)]
2377 sensor_counts: HashMap<String, u64>,
2378 #[serde(default)]
2379 label_count: u64,
2380 #[serde(default)]
2381 annotation_set_count: u64,
2382 #[serde(default)]
2383 snapshot_id: Option<u64>,
2384 #[serde(default)]
2385 is_current: bool,
2386}
2387
2388impl VersionTag {
2389 pub fn id(&self) -> u64 {
2391 self.id
2392 }
2393
2394 pub fn dataset_id(&self) -> DatasetID {
2396 self.dataset_id
2397 }
2398
2399 pub fn name(&self) -> &str {
2401 &self.name
2402 }
2403
2404 pub fn serial(&self) -> u64 {
2406 self.serial
2407 }
2408
2409 pub fn description(&self) -> &str {
2411 &self.description
2412 }
2413
2414 pub fn created_by(&self) -> &str {
2416 &self.created_by
2417 }
2418
2419 pub fn created_at(&self) -> DateTime<Utc> {
2421 self.created_at
2422 }
2423
2424 pub fn image_count(&self) -> u64 {
2426 self.image_count
2427 }
2428
2429 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2431 &self.annotation_counts
2432 }
2433
2434 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2436 &self.sensor_counts
2437 }
2438
2439 pub fn label_count(&self) -> u64 {
2441 self.label_count
2442 }
2443
2444 pub fn annotation_set_count(&self) -> u64 {
2446 self.annotation_set_count
2447 }
2448
2449 pub fn snapshot_id(&self) -> Option<u64> {
2451 self.snapshot_id
2452 }
2453
2454 pub fn is_current(&self) -> bool {
2457 self.is_current
2458 }
2459}
2460
2461impl Display for VersionTag {
2462 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2463 write!(f, "{} (serial {})", self.name, self.serial)
2464 }
2465}
2466
2467#[derive(Deserialize, Serialize, Clone, Debug)]
2469pub struct ChangelogEntry {
2470 id: u64,
2471 dataset_id: DatasetID,
2472 serial: u64,
2473 entity_type: String,
2474 operation: String,
2475 #[serde(default)]
2476 entity_id: Option<u64>,
2477 #[serde(default)]
2478 change_data: serde_json::Value,
2479 username: String,
2480 organization_id: u64,
2481 created_at: DateTime<Utc>,
2482 #[serde(default)]
2483 message: String,
2484 #[serde(default, deserialize_with = "deserialize_null_as_default")]
2485 s3_version_ids: Vec<serde_json::Value>,
2486}
2487
2488impl ChangelogEntry {
2489 pub fn id(&self) -> u64 {
2490 self.id
2491 }
2492
2493 pub fn dataset_id(&self) -> DatasetID {
2494 self.dataset_id
2495 }
2496
2497 pub fn serial(&self) -> u64 {
2499 self.serial
2500 }
2501
2502 pub fn entity_type(&self) -> &str {
2504 &self.entity_type
2505 }
2506
2507 pub fn operation(&self) -> &str {
2509 &self.operation
2510 }
2511
2512 pub fn entity_id(&self) -> Option<u64> {
2513 self.entity_id
2514 }
2515
2516 pub fn change_data(&self) -> &serde_json::Value {
2518 &self.change_data
2519 }
2520
2521 pub fn username(&self) -> &str {
2522 &self.username
2523 }
2524
2525 pub fn organization_id(&self) -> u64 {
2526 self.organization_id
2527 }
2528
2529 pub fn created_at(&self) -> DateTime<Utc> {
2530 self.created_at
2531 }
2532
2533 pub fn message(&self) -> &str {
2534 &self.message
2535 }
2536
2537 pub fn s3_version_ids(&self) -> &[serde_json::Value] {
2538 &self.s3_version_ids
2539 }
2540}
2541
2542#[derive(Deserialize, Debug, Clone)]
2544pub struct ChangelogResponse {
2545 pub entries: Vec<ChangelogEntry>,
2546 pub count: u64,
2547 #[serde(default)]
2548 pub continue_token: String,
2549 #[serde(default)]
2550 pub from_serial: Option<u64>,
2551 #[serde(default)]
2552 pub to_serial: Option<u64>,
2553}
2554
2555#[derive(Deserialize, Serialize, Clone, Debug)]
2557pub struct DatasetSummary {
2558 dataset_id: DatasetID,
2559 current_serial: u64,
2560 #[serde(default)]
2561 image_count: u64,
2562 #[serde(default)]
2563 annotation_counts: HashMap<String, u64>,
2564 #[serde(default)]
2565 sensor_counts: HashMap<String, u64>,
2566 #[serde(default)]
2567 label_count: u64,
2568 #[serde(default)]
2569 annotation_set_count: u64,
2570 last_updated: DateTime<Utc>,
2571}
2572
2573impl DatasetSummary {
2574 pub fn dataset_id(&self) -> DatasetID {
2575 self.dataset_id
2576 }
2577
2578 pub fn current_serial(&self) -> u64 {
2579 self.current_serial
2580 }
2581
2582 pub fn image_count(&self) -> u64 {
2583 self.image_count
2584 }
2585
2586 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2587 &self.annotation_counts
2588 }
2589
2590 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2591 &self.sensor_counts
2592 }
2593
2594 pub fn label_count(&self) -> u64 {
2595 self.label_count
2596 }
2597
2598 pub fn annotation_set_count(&self) -> u64 {
2599 self.annotation_set_count
2600 }
2601
2602 pub fn last_updated(&self) -> DateTime<Utc> {
2603 self.last_updated
2604 }
2605}
2606
2607#[derive(Deserialize, Debug, Clone)]
2609pub struct VersionCurrentResponse {
2610 pub dataset_id: DatasetID,
2611 pub current_serial: u64,
2612 #[serde(default)]
2613 pub latest_tag: Option<VersionTag>,
2614 #[serde(default)]
2615 pub tags: Vec<VersionTag>,
2616 #[serde(default)]
2617 pub summary: Option<DatasetSummary>,
2618}
2619
2620#[derive(Deserialize, Debug, Clone)]
2622pub struct RestoredFrom {
2623 pub tag: String,
2624 pub serial: u64,
2625}
2626
2627#[derive(Deserialize, Debug, Clone)]
2629pub struct RestoredCounts {
2630 pub images: u64,
2631 pub labels: u64,
2632 pub annotation_sets: u64,
2633}
2634
2635#[derive(Deserialize, Debug, Clone)]
2637pub struct RestoreResult {
2638 pub success: bool,
2639 pub new_serial: u64,
2640 pub restored_from: RestoredFrom,
2641 pub restored_counts: RestoredCounts,
2642 pub message: String,
2643}
2644
2645#[derive(Serialize)]
2648pub(crate) struct VersionTagCreateParams {
2649 pub dataset_id: DatasetID,
2650 pub name: String,
2651 #[serde(skip_serializing_if = "Option::is_none")]
2652 pub description: Option<String>,
2653}
2654
2655#[derive(Serialize)]
2656pub(crate) struct VersionTagNameParams {
2657 pub dataset_id: DatasetID,
2658 pub name: String,
2659}
2660
2661#[derive(Serialize)]
2662pub(crate) struct VersionChangelogParams {
2663 pub dataset_id: DatasetID,
2664 #[serde(skip_serializing_if = "Option::is_none")]
2665 pub from_version: Option<String>,
2666 #[serde(skip_serializing_if = "Option::is_none")]
2667 pub to_version: Option<String>,
2668 #[serde(skip_serializing_if = "Option::is_none")]
2669 pub entity_types: Option<Vec<String>>,
2670 #[serde(skip_serializing_if = "Option::is_none")]
2671 pub limit: Option<u64>,
2672 #[serde(skip_serializing_if = "Option::is_none")]
2673 pub continue_token: Option<String>,
2674}
2675
2676#[derive(Deserialize, Debug)]
2678pub(crate) struct ChangelogCountResult {
2679 pub count: u64,
2680}
2681
2682#[derive(Serialize, Deserialize, Debug, Clone)]
2689pub struct TrainerSchemaInfo {
2690 pub name: String,
2692 #[serde(default)]
2694 pub label: String,
2695 #[serde(default)]
2697 pub schema_type: String,
2698}
2699
2700#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
2706#[serde(rename_all = "lowercase")]
2707pub enum SchemaFieldType {
2708 Group,
2710 Slider,
2712 Select,
2714 Bool,
2716 Int,
2718 Float,
2720 Text,
2722 Date,
2724 Project,
2726 Dataset,
2728 Trainer,
2730 Upload,
2732 Info,
2735 #[serde(other)]
2737 Unknown,
2738}
2739
2740fn lenient_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
2744where
2745 D: Deserializer<'de>,
2746{
2747 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
2748 Ok(value.map(|v| match v {
2749 serde_json::Value::String(s) => s,
2750 other => other.to_string(),
2751 }))
2752}
2753
2754#[derive(Serialize, Deserialize, Debug, Clone)]
2756pub struct SchemaOption {
2757 #[serde(default)]
2759 pub name: Option<Parameter>,
2760 #[serde(default, deserialize_with = "lenient_string")]
2763 pub label: Option<String>,
2764 #[serde(default)]
2766 pub children: Vec<SchemaField>,
2767}
2768
2769#[derive(Serialize, Deserialize, Debug, Clone)]
2781pub struct SchemaField {
2782 #[serde(default, deserialize_with = "lenient_string")]
2784 pub name: Option<String>,
2785 #[serde(default, deserialize_with = "lenient_string")]
2787 pub label: Option<String>,
2788 #[serde(default, deserialize_with = "lenient_string")]
2790 pub description: Option<String>,
2791 #[serde(default)]
2793 pub required: bool,
2794 #[serde(default)]
2796 pub default: Option<Parameter>,
2797 #[serde(rename = "type", default)]
2799 pub field_type: Option<SchemaFieldType>,
2800 #[serde(default)]
2802 pub min: Option<f64>,
2803 #[serde(default)]
2805 pub max: Option<f64>,
2806 #[serde(default)]
2808 pub step: Option<f64>,
2809 #[serde(default)]
2811 pub options: Vec<SchemaOption>,
2812 #[serde(default)]
2815 pub children: Vec<SchemaField>,
2816 #[serde(default)]
2818 pub is_dropdown: bool,
2819 #[serde(default)]
2821 pub multi_select: bool,
2822 #[serde(default)]
2824 pub is_multi_line: bool,
2825 #[serde(default)]
2827 pub hidden: bool,
2828 #[serde(default)]
2830 pub numeric_only: bool,
2831 #[serde(default)]
2833 pub enable_tags_selection: bool,
2834 #[serde(default)]
2836 pub enable_annotation_set_selection: bool,
2837 #[serde(default)]
2839 pub values: Option<Vec<Parameter>>,
2840}
2841
2842#[derive(Serialize, Deserialize, Debug, Clone)]
2845pub struct ValidatorSchema {
2846 #[serde(rename = "type", default)]
2848 pub schema_type: String,
2849 #[serde(default)]
2851 pub name: String,
2852 #[serde(default)]
2854 pub schema: Vec<SchemaField>,
2855}
2856
2857#[cfg(test)]
2858mod tests {
2859 use super::*;
2860
2861 #[test]
2863 fn test_organization_id_from_u64() {
2864 let id = OrganizationID::from(12345);
2865 assert_eq!(id.value(), 12345);
2866 }
2867
2868 #[test]
2869 fn test_organization_id_display() {
2870 let id = OrganizationID::from(0xabc123);
2871 assert_eq!(format!("{}", id), "org-abc123");
2872 }
2873
2874 #[test]
2875 fn test_organization_id_try_from_str_valid() {
2876 let id = OrganizationID::try_from("org-abc123").unwrap();
2877 assert_eq!(id.value(), 0xabc123);
2878 }
2879
2880 #[test]
2881 fn test_organization_id_try_from_str_invalid_prefix() {
2882 let result = OrganizationID::try_from("invalid-abc123");
2883 assert!(result.is_err());
2884 match result {
2885 Err(Error::InvalidParameters(msg)) => {
2886 assert!(msg.contains("must start with 'org-'"));
2887 }
2888 _ => panic!("Expected InvalidParameters error"),
2889 }
2890 }
2891
2892 #[test]
2893 fn test_organization_id_try_from_str_invalid_hex() {
2894 let result = OrganizationID::try_from("org-xyz");
2895 assert!(result.is_err());
2896 }
2897
2898 #[test]
2899 fn test_organization_id_try_from_str_empty() {
2900 let result = OrganizationID::try_from("org-");
2901 assert!(result.is_err());
2902 }
2903
2904 #[test]
2905 fn test_organization_id_into_u64() {
2906 let id = OrganizationID::from(54321);
2907 let value: u64 = id.into();
2908 assert_eq!(value, 54321);
2909 }
2910
2911 #[test]
2913 fn test_usage_summary_deserialize_and_accessors() {
2914 let usage: UsageSummary = serde_json::from_str(
2915 r#"{"credits": 12.5, "funds": 49092.92, "total_funds_and_credits": 49105.42}"#,
2916 )
2917 .unwrap();
2918 assert_eq!(usage.credits(), 12.5);
2919 assert_eq!(usage.funds(), 49092.92);
2920 assert_eq!(usage.total(), 49105.42);
2921 }
2922
2923 #[test]
2924 fn test_usage_summary_defaults_for_missing_fields() {
2925 let usage: UsageSummary = serde_json::from_str("{}").unwrap();
2929 assert_eq!(usage.credits(), 0.0);
2930 assert_eq!(usage.funds(), 0.0);
2931 assert_eq!(usage.total(), 0.0);
2932 }
2933
2934 #[test]
2936 fn test_project_id_from_u64() {
2937 let id = ProjectID::from(78910);
2938 assert_eq!(id.value(), 78910);
2939 }
2940
2941 #[test]
2942 fn test_project_id_display() {
2943 let id = ProjectID::from(0xdef456);
2944 assert_eq!(format!("{}", id), "p-def456");
2945 }
2946
2947 #[test]
2948 fn test_project_id_from_str_valid() {
2949 let id = ProjectID::from_str("p-def456").unwrap();
2950 assert_eq!(id.value(), 0xdef456);
2951 }
2952
2953 #[test]
2954 fn test_project_id_try_from_str_valid() {
2955 let id = ProjectID::try_from("p-123abc").unwrap();
2956 assert_eq!(id.value(), 0x123abc);
2957 }
2958
2959 #[test]
2960 fn test_project_id_try_from_string_valid() {
2961 let id = ProjectID::try_from("p-456def".to_string()).unwrap();
2962 assert_eq!(id.value(), 0x456def);
2963 }
2964
2965 #[test]
2966 fn test_project_id_from_str_invalid_prefix() {
2967 let result = ProjectID::from_str("proj-123");
2968 assert!(result.is_err());
2969 match result {
2970 Err(Error::InvalidParameters(msg)) => {
2971 assert!(msg.contains("must start with 'p-'"));
2972 }
2973 _ => panic!("Expected InvalidParameters error"),
2974 }
2975 }
2976
2977 #[test]
2978 fn test_project_id_from_str_invalid_hex() {
2979 let result = ProjectID::from_str("p-notahex");
2980 assert!(result.is_err());
2981 }
2982
2983 #[test]
2984 fn test_project_id_into_u64() {
2985 let id = ProjectID::from(99999);
2986 let value: u64 = id.into();
2987 assert_eq!(value, 99999);
2988 }
2989
2990 #[test]
2992 fn test_experiment_id_from_u64() {
2993 let id = ExperimentID::from(1193046);
2994 assert_eq!(id.value(), 1193046);
2995 }
2996
2997 #[test]
2998 fn test_experiment_id_display() {
2999 let id = ExperimentID::from(0x123abc);
3000 assert_eq!(format!("{}", id), "exp-123abc");
3001 }
3002
3003 #[test]
3004 fn test_experiment_id_from_str_valid() {
3005 let id = ExperimentID::from_str("exp-456def").unwrap();
3006 assert_eq!(id.value(), 0x456def);
3007 }
3008
3009 #[test]
3010 fn test_experiment_id_try_from_str_valid() {
3011 let id = ExperimentID::try_from("exp-789abc").unwrap();
3012 assert_eq!(id.value(), 0x789abc);
3013 }
3014
3015 #[test]
3016 fn test_experiment_id_try_from_string_valid() {
3017 let id = ExperimentID::try_from("exp-fedcba".to_string()).unwrap();
3018 assert_eq!(id.value(), 0xfedcba);
3019 }
3020
3021 #[test]
3022 fn test_experiment_id_from_str_invalid_prefix() {
3023 let result = ExperimentID::from_str("experiment-123");
3024 assert!(result.is_err());
3025 match result {
3026 Err(Error::InvalidParameters(msg)) => {
3027 assert!(msg.contains("must start with 'exp-'"));
3028 }
3029 _ => panic!("Expected InvalidParameters error"),
3030 }
3031 }
3032
3033 #[test]
3034 fn test_experiment_id_from_str_invalid_hex() {
3035 let result = ExperimentID::from_str("exp-zzz");
3036 assert!(result.is_err());
3037 }
3038
3039 #[test]
3040 fn test_experiment_id_into_u64() {
3041 let id = ExperimentID::from(777777);
3042 let value: u64 = id.into();
3043 assert_eq!(value, 777777);
3044 }
3045
3046 #[test]
3048 fn test_training_session_id_from_u64() {
3049 let id = TrainingSessionID::from(7901234);
3050 assert_eq!(id.value(), 7901234);
3051 }
3052
3053 #[test]
3054 fn test_training_session_id_display() {
3055 let id = TrainingSessionID::from(0xabc123);
3056 assert_eq!(format!("{}", id), "t-abc123");
3057 }
3058
3059 #[test]
3060 fn test_training_session_id_from_str_valid() {
3061 let id = TrainingSessionID::from_str("t-abc123").unwrap();
3062 assert_eq!(id.value(), 0xabc123);
3063 }
3064
3065 #[test]
3066 fn test_training_session_id_try_from_str_valid() {
3067 let id = TrainingSessionID::try_from("t-deadbeef").unwrap();
3068 assert_eq!(id.value(), 0xdeadbeef);
3069 }
3070
3071 #[test]
3072 fn test_training_session_id_try_from_string_valid() {
3073 let id = TrainingSessionID::try_from("t-cafebabe".to_string()).unwrap();
3074 assert_eq!(id.value(), 0xcafebabe);
3075 }
3076
3077 #[test]
3078 fn test_training_session_id_from_str_invalid_prefix() {
3079 let result = TrainingSessionID::from_str("training-123");
3080 assert!(result.is_err());
3081 match result {
3082 Err(Error::InvalidParameters(msg)) => {
3083 assert!(msg.contains("must start with 't-'"));
3084 }
3085 _ => panic!("Expected InvalidParameters error"),
3086 }
3087 }
3088
3089 #[test]
3090 fn test_training_session_id_from_str_invalid_hex() {
3091 let result = TrainingSessionID::from_str("t-qqq");
3092 assert!(result.is_err());
3093 }
3094
3095 #[test]
3096 fn test_training_session_id_into_u64() {
3097 let id = TrainingSessionID::from(123456);
3098 let value: u64 = id.into();
3099 assert_eq!(value, 123456);
3100 }
3101
3102 #[test]
3104 fn test_validation_session_id_from_u64() {
3105 let id = ValidationSessionID::from(3456789);
3106 assert_eq!(id.value(), 3456789);
3107 }
3108
3109 #[test]
3110 fn test_validation_session_id_display() {
3111 let id = ValidationSessionID::from(0x34c985);
3112 assert_eq!(format!("{}", id), "v-34c985");
3113 }
3114
3115 #[test]
3116 fn test_validation_session_id_try_from_str_valid() {
3117 let id = ValidationSessionID::try_from("v-deadbeef").unwrap();
3118 assert_eq!(id.value(), 0xdeadbeef);
3119 }
3120
3121 #[test]
3122 fn test_validation_session_id_try_from_string_valid() {
3123 let id = ValidationSessionID::try_from("v-12345678".to_string()).unwrap();
3124 assert_eq!(id.value(), 0x12345678);
3125 }
3126
3127 #[test]
3128 fn test_validation_session_id_try_from_str_invalid_prefix() {
3129 let result = ValidationSessionID::try_from("validation-123");
3130 assert!(result.is_err());
3131 match result {
3132 Err(Error::InvalidParameters(msg)) => {
3133 assert!(msg.contains("must start with 'v-'"));
3134 }
3135 _ => panic!("Expected InvalidParameters error"),
3136 }
3137 }
3138
3139 #[test]
3140 fn test_validation_session_id_try_from_str_invalid_hex() {
3141 let result = ValidationSessionID::try_from("v-xyz");
3142 assert!(result.is_err());
3143 }
3144
3145 #[test]
3146 fn test_validation_session_id_into_u64() {
3147 let id = ValidationSessionID::from(987654);
3148 let value: u64 = id.into();
3149 assert_eq!(value, 987654);
3150 }
3151
3152 #[test]
3154 fn test_snapshot_id_from_u64() {
3155 let id = SnapshotID::from(111222);
3156 assert_eq!(id.value(), 111222);
3157 }
3158
3159 #[test]
3160 fn test_snapshot_id_display() {
3161 let id = SnapshotID::from(0xaabbcc);
3162 assert_eq!(format!("{}", id), "ss-aabbcc");
3163 }
3164
3165 #[test]
3166 fn test_snapshot_id_try_from_str_valid() {
3167 let id = SnapshotID::try_from("ss-aabbcc").unwrap();
3168 assert_eq!(id.value(), 0xaabbcc);
3169 }
3170
3171 #[test]
3172 fn test_snapshot_id_try_from_str_invalid_prefix() {
3173 let result = SnapshotID::try_from("snapshot-123");
3174 assert!(result.is_err());
3175 match result {
3176 Err(Error::InvalidParameters(msg)) => {
3177 assert!(msg.contains("must start with 'ss-'"));
3178 }
3179 _ => panic!("Expected InvalidParameters error"),
3180 }
3181 }
3182
3183 #[test]
3184 fn test_snapshot_id_try_from_str_invalid_hex() {
3185 let result = SnapshotID::try_from("ss-ggg");
3186 assert!(result.is_err());
3187 }
3188
3189 #[test]
3190 fn test_snapshot_id_into_u64() {
3191 let id = SnapshotID::from(333444);
3192 let value: u64 = id.into();
3193 assert_eq!(value, 333444);
3194 }
3195
3196 #[test]
3199 fn test_background_task_id_parses_bt_prefix() {
3200 let id = BackgroundTaskID::from_str("bt-55b5").unwrap();
3203 assert_eq!(id.value(), 0x55b5);
3204 }
3205
3206 #[test]
3207 fn test_background_task_id_display_round_trips() {
3208 let id = BackgroundTaskID::from(0x55b5);
3209 assert_eq!(id.to_string(), "bt-55b5");
3210 assert_eq!(BackgroundTaskID::from_str("bt-55b5").unwrap(), id);
3211 }
3212
3213 #[test]
3214 fn test_background_task_id_rejects_other_prefixes() {
3215 for s in ["task-55b5", "55b5", "b-55b5", "bt55b5"] {
3219 let result = BackgroundTaskID::from_str(s);
3220 assert!(result.is_err(), "expected {s} to be rejected");
3221 }
3222 }
3223
3224 #[test]
3225 fn test_background_task_id_rejects_invalid_hex() {
3226 assert!(BackgroundTaskID::from_str("bt-ggg").is_err());
3227 }
3228
3229 #[test]
3230 fn test_background_task_id_converts_to_task_id() {
3231 let bt = BackgroundTaskID::from_str("bt-55b5").unwrap();
3234 let task: TaskID = bt.into();
3235 assert_eq!(task.value(), bt.value());
3236 assert_eq!(task.to_string(), "task-55b5");
3237 }
3238
3239 #[test]
3240 fn test_task_id_converts_to_background_task_id() {
3241 let task = TaskID::from_str("task-abc123").unwrap();
3242 let bt: BackgroundTaskID = task.into();
3243 assert_eq!(bt.value(), task.value());
3244 assert_eq!(bt.to_string(), "bt-abc123");
3245 }
3246
3247 #[test]
3248 fn test_background_task_id_conversion_is_lossless() {
3249 for raw in [0u64, 1, 0x55b5, u64::MAX] {
3250 let bt = BackgroundTaskID::from(raw);
3251 let round: BackgroundTaskID = TaskID::from(bt).into();
3252 assert_eq!(round.value(), raw);
3253 }
3254 }
3255
3256 #[test]
3258 fn test_task_id_from_u64() {
3259 let id = TaskID::from(555666);
3260 assert_eq!(id.value(), 555666);
3261 }
3262
3263 #[test]
3264 fn test_task_id_display() {
3265 let id = TaskID::from(0x123456);
3266 assert_eq!(format!("{}", id), "task-123456");
3267 }
3268
3269 #[test]
3270 fn test_task_id_from_str_valid() {
3271 let id = TaskID::from_str("task-123456").unwrap();
3272 assert_eq!(id.value(), 0x123456);
3273 }
3274
3275 #[test]
3276 fn test_task_id_try_from_str_valid() {
3277 let id = TaskID::try_from("task-abcdef").unwrap();
3278 assert_eq!(id.value(), 0xabcdef);
3279 }
3280
3281 #[test]
3282 fn test_task_id_try_from_string_valid() {
3283 let id = TaskID::try_from("task-fedcba".to_string()).unwrap();
3284 assert_eq!(id.value(), 0xfedcba);
3285 }
3286
3287 #[test]
3288 fn test_task_id_from_str_invalid_prefix() {
3289 let result = TaskID::from_str("t-123");
3290 assert!(result.is_err());
3291 match result {
3292 Err(Error::InvalidParameters(msg)) => {
3293 assert!(msg.contains("must start with 'task-'"));
3294 }
3295 _ => panic!("Expected InvalidParameters error"),
3296 }
3297 }
3298
3299 #[test]
3300 fn test_task_id_from_str_invalid_hex() {
3301 let result = TaskID::from_str("task-zzz");
3302 assert!(result.is_err());
3303 }
3304
3305 #[test]
3306 fn test_task_id_into_u64() {
3307 let id = TaskID::from(777888);
3308 let value: u64 = id.into();
3309 assert_eq!(value, 777888);
3310 }
3311
3312 #[test]
3314 fn test_dataset_id_from_u64() {
3315 let id = DatasetID::from(1193046);
3316 assert_eq!(id.value(), 1193046);
3317 }
3318
3319 #[test]
3320 fn test_dataset_id_display() {
3321 let id = DatasetID::from(0x123abc);
3322 assert_eq!(format!("{}", id), "ds-123abc");
3323 }
3324
3325 #[test]
3326 fn test_dataset_id_from_str_valid() {
3327 let id = DatasetID::from_str("ds-456def").unwrap();
3328 assert_eq!(id.value(), 0x456def);
3329 }
3330
3331 #[test]
3332 fn test_dataset_id_try_from_str_valid() {
3333 let id = DatasetID::try_from("ds-789abc").unwrap();
3334 assert_eq!(id.value(), 0x789abc);
3335 }
3336
3337 #[test]
3338 fn test_dataset_id_try_from_string_valid() {
3339 let id = DatasetID::try_from("ds-fedcba".to_string()).unwrap();
3340 assert_eq!(id.value(), 0xfedcba);
3341 }
3342
3343 #[test]
3344 fn test_dataset_id_from_str_invalid_prefix() {
3345 let result = DatasetID::from_str("dataset-123");
3346 assert!(result.is_err());
3347 match result {
3348 Err(Error::InvalidParameters(msg)) => {
3349 assert!(msg.contains("must start with 'ds-'"));
3350 }
3351 _ => panic!("Expected InvalidParameters error"),
3352 }
3353 }
3354
3355 #[test]
3356 fn test_dataset_id_from_str_invalid_hex() {
3357 let result = DatasetID::from_str("ds-zzz");
3358 assert!(result.is_err());
3359 }
3360
3361 #[test]
3362 fn test_dataset_id_into_u64() {
3363 let id = DatasetID::from(111111);
3364 let value: u64 = id.into();
3365 assert_eq!(value, 111111);
3366 }
3367
3368 #[test]
3369 fn dataset_id_default_is_zero() {
3370 assert_eq!(DatasetID::default().value(), 0);
3371 }
3372
3373 #[test]
3374 fn dataset_params_default_is_all_zero_and_empty() {
3375 let params = DatasetParams::default();
3376 assert_eq!(params.dataset_id().value(), 0);
3377 assert_eq!(params.annotation_set_id().value(), 0);
3378 assert_eq!(params.train_group(), "");
3379 assert_eq!(params.val_group(), "");
3380 }
3381
3382 #[test]
3384 fn test_annotation_set_id_from_u64() {
3385 let id = AnnotationSetID::from(222333);
3386 assert_eq!(id.value(), 222333);
3387 }
3388
3389 #[test]
3390 fn test_annotation_set_id_display() {
3391 let id = AnnotationSetID::from(0xabcdef);
3392 assert_eq!(format!("{}", id), "as-abcdef");
3393 }
3394
3395 #[test]
3396 fn test_annotation_set_id_from_str_valid() {
3397 let id = AnnotationSetID::from_str("as-abcdef").unwrap();
3398 assert_eq!(id.value(), 0xabcdef);
3399 }
3400
3401 #[test]
3402 fn test_annotation_set_id_try_from_str_valid() {
3403 let id = AnnotationSetID::try_from("as-123456").unwrap();
3404 assert_eq!(id.value(), 0x123456);
3405 }
3406
3407 #[test]
3408 fn test_annotation_set_id_try_from_string_valid() {
3409 let id = AnnotationSetID::try_from("as-fedcba".to_string()).unwrap();
3410 assert_eq!(id.value(), 0xfedcba);
3411 }
3412
3413 #[test]
3414 fn test_annotation_set_id_from_str_invalid_prefix() {
3415 let result = AnnotationSetID::from_str("annotation-123");
3416 assert!(result.is_err());
3417 match result {
3418 Err(Error::InvalidParameters(msg)) => {
3419 assert!(msg.contains("must start with 'as-'"));
3420 }
3421 _ => panic!("Expected InvalidParameters error"),
3422 }
3423 }
3424
3425 #[test]
3426 fn test_annotation_set_id_from_str_invalid_hex() {
3427 let result = AnnotationSetID::from_str("as-zzz");
3428 assert!(result.is_err());
3429 }
3430
3431 #[test]
3432 fn test_annotation_set_id_into_u64() {
3433 let id = AnnotationSetID::from(444555);
3434 let value: u64 = id.into();
3435 assert_eq!(value, 444555);
3436 }
3437
3438 #[test]
3440 fn test_sample_id_from_u64() {
3441 let id = SampleID::from(666777);
3442 assert_eq!(id.value(), 666777);
3443 }
3444
3445 #[test]
3446 fn test_sample_id_display() {
3447 let id = SampleID::from(0x987654);
3448 assert_eq!(format!("{}", id), "s-987654");
3449 }
3450
3451 #[test]
3452 fn test_sample_id_try_from_str_valid() {
3453 let id = SampleID::try_from("s-987654").unwrap();
3454 assert_eq!(id.value(), 0x987654);
3455 }
3456
3457 #[test]
3458 fn test_sample_id_try_from_str_invalid_prefix() {
3459 let result = SampleID::try_from("sample-123");
3460 assert!(result.is_err());
3461 match result {
3462 Err(Error::InvalidParameters(msg)) => {
3463 assert!(msg.contains("must start with 's-'"));
3464 }
3465 _ => panic!("Expected InvalidParameters error"),
3466 }
3467 }
3468
3469 #[test]
3470 fn test_sample_id_try_from_str_invalid_hex() {
3471 let result = SampleID::try_from("s-zzz");
3472 assert!(result.is_err());
3473 }
3474
3475 #[test]
3476 fn test_sample_id_into_u64() {
3477 let id = SampleID::from(888999);
3478 let value: u64 = id.into();
3479 assert_eq!(value, 888999);
3480 }
3481
3482 #[test]
3484 fn test_app_id_from_u64() {
3485 let id = AppId::from(123123);
3486 assert_eq!(id.value(), 123123);
3487 }
3488
3489 #[test]
3490 fn test_app_id_display() {
3491 let id = AppId::from(0x456789);
3492 assert_eq!(format!("{}", id), "app-456789");
3493 }
3494
3495 #[test]
3496 fn test_app_id_try_from_str_valid() {
3497 let id = AppId::try_from("app-456789").unwrap();
3498 assert_eq!(id.value(), 0x456789);
3499 }
3500
3501 #[test]
3502 fn test_app_id_try_from_str_invalid_prefix() {
3503 let result = AppId::try_from("application-123");
3504 assert!(result.is_err());
3505 match result {
3506 Err(Error::InvalidParameters(msg)) => {
3507 assert!(msg.contains("must start with 'app-'"));
3508 }
3509 _ => panic!("Expected InvalidParameters error"),
3510 }
3511 }
3512
3513 #[test]
3514 fn test_app_id_try_from_str_invalid_hex() {
3515 let result = AppId::try_from("app-zzz");
3516 assert!(result.is_err());
3517 }
3518
3519 #[test]
3520 fn test_app_id_into_u64() {
3521 let id = AppId::from(321321);
3522 let value: u64 = id.into();
3523 assert_eq!(value, 321321);
3524 }
3525
3526 #[test]
3528 fn test_image_id_from_u64() {
3529 let id = ImageId::from(789789);
3530 assert_eq!(id.value(), 789789);
3531 }
3532
3533 #[test]
3534 fn test_image_id_display() {
3535 let id = ImageId::from(0xabcd1234);
3536 assert_eq!(format!("{}", id), "im-abcd1234");
3537 }
3538
3539 #[test]
3540 fn test_image_id_try_from_str_valid() {
3541 let id = ImageId::try_from("im-abcd1234").unwrap();
3542 assert_eq!(id.value(), 0xabcd1234);
3543 }
3544
3545 #[test]
3546 fn test_image_id_try_from_str_invalid_prefix() {
3547 let result = ImageId::try_from("image-123");
3548 assert!(result.is_err());
3549 match result {
3550 Err(Error::InvalidParameters(msg)) => {
3551 assert!(msg.contains("must start with 'im-'"));
3552 }
3553 _ => panic!("Expected InvalidParameters error"),
3554 }
3555 }
3556
3557 #[test]
3558 fn test_image_id_try_from_str_invalid_hex() {
3559 let result = ImageId::try_from("im-zzz");
3560 assert!(result.is_err());
3561 }
3562
3563 #[test]
3564 fn test_image_id_into_u64() {
3565 let id = ImageId::from(987987);
3566 let value: u64 = id.into();
3567 assert_eq!(value, 987987);
3568 }
3569
3570 #[test]
3572 fn test_id_types_equality() {
3573 let id1 = ProjectID::from(12345);
3574 let id2 = ProjectID::from(12345);
3575 let id3 = ProjectID::from(54321);
3576
3577 assert_eq!(id1, id2);
3578 assert_ne!(id1, id3);
3579 }
3580
3581 #[test]
3582 fn test_id_types_hash() {
3583 use std::collections::HashSet;
3584
3585 let mut set = HashSet::new();
3586 set.insert(DatasetID::from(100));
3587 set.insert(DatasetID::from(200));
3588 set.insert(DatasetID::from(100)); assert_eq!(set.len(), 2);
3591 assert!(set.contains(&DatasetID::from(100)));
3592 assert!(set.contains(&DatasetID::from(200)));
3593 }
3594
3595 #[test]
3596 fn test_id_types_copy_clone() {
3597 let id1 = ExperimentID::from(999);
3598 let id2 = id1; let id3 = id1; assert_eq!(id1, id2);
3602 assert_eq!(id1, id3);
3603 }
3604
3605 #[test]
3607 fn test_id_zero_value() {
3608 let id = ProjectID::from(0);
3609 assert_eq!(format!("{}", id), "p-0");
3610 assert_eq!(id.value(), 0);
3611 }
3612
3613 #[test]
3614 fn test_id_max_value() {
3615 let id = ProjectID::from(u64::MAX);
3616 assert_eq!(format!("{}", id), "p-ffffffffffffffff");
3617 assert_eq!(id.value(), u64::MAX);
3618 }
3619
3620 #[test]
3621 fn test_id_round_trip_conversion() {
3622 let original = 0xdeadbeef_u64;
3623 let id = TrainingSessionID::from(original);
3624 let back: u64 = id.into();
3625 assert_eq!(original, back);
3626 }
3627
3628 #[test]
3629 fn test_id_case_insensitive_hex() {
3630 let id1 = DatasetID::from_str("ds-ABCDEF").unwrap();
3632 let id2 = DatasetID::from_str("ds-abcdef").unwrap();
3633 assert_eq!(id1.value(), id2.value());
3634 }
3635
3636 #[test]
3637 fn test_id_with_leading_zeros() {
3638 let id = ProjectID::from_str("p-00001234").unwrap();
3639 assert_eq!(id.value(), 0x1234);
3640 }
3641
3642 #[test]
3644 fn test_parameter_integer() {
3645 let param = Parameter::Integer(42);
3646 match param {
3647 Parameter::Integer(val) => assert_eq!(val, 42),
3648 _ => panic!("Expected Integer variant"),
3649 }
3650 }
3651
3652 #[test]
3653 fn test_parameter_real() {
3654 let param = Parameter::Real(2.5);
3655 match param {
3656 Parameter::Real(val) => assert_eq!(val, 2.5),
3657 _ => panic!("Expected Real variant"),
3658 }
3659 }
3660
3661 #[test]
3662 fn test_parameter_boolean() {
3663 let param = Parameter::Boolean(true);
3664 match param {
3665 Parameter::Boolean(val) => assert!(val),
3666 _ => panic!("Expected Boolean variant"),
3667 }
3668 }
3669
3670 #[test]
3671 fn test_parameter_string() {
3672 let param = Parameter::String("test".to_string());
3673 match param {
3674 Parameter::String(val) => assert_eq!(val, "test"),
3675 _ => panic!("Expected String variant"),
3676 }
3677 }
3678
3679 #[test]
3680 fn test_parameter_array() {
3681 let param = Parameter::Array(vec![
3682 Parameter::Integer(1),
3683 Parameter::Integer(2),
3684 Parameter::Integer(3),
3685 ]);
3686 match param {
3687 Parameter::Array(arr) => assert_eq!(arr.len(), 3),
3688 _ => panic!("Expected Array variant"),
3689 }
3690 }
3691
3692 #[test]
3693 fn test_parameter_object() {
3694 let mut map = HashMap::new();
3695 map.insert("key".to_string(), Parameter::Integer(100));
3696 let param = Parameter::Object(map);
3697 match param {
3698 Parameter::Object(obj) => {
3699 assert_eq!(obj.len(), 1);
3700 assert!(obj.contains_key("key"));
3701 }
3702 _ => panic!("Expected Object variant"),
3703 }
3704 }
3705
3706 #[test]
3707 fn test_parameter_clone() {
3708 let param1 = Parameter::Integer(42);
3709 let param2 = param1.clone();
3710 assert_eq!(param1, param2);
3711 }
3712
3713 #[test]
3714 fn test_parameter_nested() {
3715 let inner_array = Parameter::Array(vec![Parameter::Integer(1), Parameter::Integer(2)]);
3716 let outer_array = Parameter::Array(vec![inner_array.clone(), inner_array]);
3717
3718 match outer_array {
3719 Parameter::Array(arr) => {
3720 assert_eq!(arr.len(), 2);
3721 }
3722 _ => panic!("Expected Array variant"),
3723 }
3724 }
3725
3726 macro_rules! test_typeid_conversions {
3729 ($test_name:ident, $type:ty, $prefix:literal, $wrong_prefix:literal) => {
3730 #[test]
3731 fn $test_name() {
3732 let id = <$type>::from(0xabc123);
3734 assert_eq!(id.value(), 0xabc123);
3735
3736 assert_eq!(format!("{}", id), concat!($prefix, "-abc123"));
3738
3739 let id: $type = concat!($prefix, "-abc123").parse().unwrap();
3741 assert_eq!(id.value(), 0xabc123);
3742
3743 assert!(concat!($wrong_prefix, "-abc").parse::<$type>().is_err());
3745
3746 assert!("abc123".parse::<$type>().is_err());
3748
3749 assert!(concat!($prefix, "-xyz").parse::<$type>().is_err());
3751
3752 let id = <$type>::try_from(concat!($prefix, "-abc123")).unwrap();
3754 assert_eq!(id.value(), 0xabc123);
3755
3756 let id = <$type>::try_from(concat!($prefix, "-abc123").to_string()).unwrap();
3758 assert_eq!(id.value(), 0xabc123);
3759
3760 let id = <$type>::from(0xabc123);
3762 let json = serde_json::to_string(&id).unwrap();
3763 let parsed: $type = serde_json::from_str(&json).unwrap();
3764 assert_eq!(id, parsed);
3765
3766 let id = <$type>::from(0xabc123);
3768 let val: u64 = id.into();
3769 assert_eq!(val, 0xabc123);
3770 }
3771 };
3772 }
3773
3774 test_typeid_conversions!(test_organization_id_conversions, OrganizationID, "org", "p");
3775 test_typeid_conversions!(test_project_id_conversions, ProjectID, "p", "org");
3776 test_typeid_conversions!(test_experiment_id_conversions, ExperimentID, "exp", "p");
3777 test_typeid_conversions!(
3778 test_training_session_id_conversions,
3779 TrainingSessionID,
3780 "t",
3781 "v"
3782 );
3783 test_typeid_conversions!(
3784 test_validation_session_id_conversions,
3785 ValidationSessionID,
3786 "v",
3787 "t"
3788 );
3789 test_typeid_conversions!(test_snapshot_id_conversions, SnapshotID, "ss", "ds");
3790 test_typeid_conversions!(test_task_id_conversions, TaskID, "task", "t");
3791 test_typeid_conversions!(test_dataset_id_conversions, DatasetID, "ds", "ss");
3792 test_typeid_conversions!(
3793 test_annotation_set_id_conversions,
3794 AnnotationSetID,
3795 "as",
3796 "ds"
3797 );
3798 test_typeid_conversions!(test_sample_id_conversions, SampleID, "s", "p");
3799 test_typeid_conversions!(test_app_id_conversions, AppId, "app", "p");
3800 test_typeid_conversions!(test_image_id_conversions, ImageId, "im", "se");
3801 test_typeid_conversions!(test_sequence_id_conversions, SequenceId, "se", "im");
3802
3803 #[test]
3806 fn test_version_tag_deserialize_full() {
3807 let json = r#"{
3808 "id": 456, "dataset_id": 1715004, "name": "training-v1.0",
3809 "serial": 42, "description": "Ready for production",
3810 "created_by": "user@example.com", "created_at": "2025-01-15T10:30:00Z",
3811 "image_count": 50000, "annotation_counts": {"box": 150000, "seg": 20000},
3812 "sensor_counts": {"lidar": 25000}, "label_count": 15,
3813 "annotation_set_count": 3, "snapshot_id": 789
3814 }"#;
3815 let tag: VersionTag = serde_json::from_str(json).unwrap();
3816 assert_eq!(tag.name(), "training-v1.0");
3817 assert_eq!(tag.serial(), 42);
3818 assert_eq!(tag.image_count(), 50000);
3819 assert_eq!(tag.annotation_counts().get("box"), Some(&150000));
3820 assert_eq!(tag.snapshot_id(), Some(789));
3821 }
3822
3823 #[test]
3824 fn test_version_tag_deserialize_omitempty() {
3825 let json = r#"{
3827 "id": 1, "dataset_id": 2, "name": "v1.0", "serial": 5,
3828 "description": "", "created_by": "user",
3829 "created_at": "2025-01-01T00:00:00Z"
3830 }"#;
3831 let tag: VersionTag = serde_json::from_str(json).unwrap();
3832 assert_eq!(tag.snapshot_id(), None);
3833 assert_eq!(tag.image_count(), 0);
3834 assert!(tag.annotation_counts().is_empty());
3835 }
3836
3837 #[test]
3838 fn test_changelog_entry_deserialize_omitempty() {
3839 let json = r#"{
3841 "id": 1, "dataset_id": 2, "serial": 3, "entity_type": "image",
3842 "operation": "bulk_create", "change_data": {"count": 5},
3843 "username": "user", "organization_id": 1,
3844 "created_at": "2025-01-01T00:00:00Z", "message": ""
3845 }"#;
3846 let entry: ChangelogEntry = serde_json::from_str(json).unwrap();
3847 assert!(entry.entity_id().is_none());
3848 assert!(entry.s3_version_ids().is_empty());
3849 assert_eq!(entry.entity_type(), "image");
3850 assert_eq!(entry.operation(), "bulk_create");
3851 }
3852
3853 #[test]
3854 fn test_changelog_response_deserialize() {
3855 let json = r#"{
3856 "entries": [], "count": 0, "continue_token": ""
3857 }"#;
3858 let resp: ChangelogResponse = serde_json::from_str(json).unwrap();
3859 assert!(resp.entries.is_empty());
3860 assert_eq!(resp.count, 0);
3861 assert!(resp.continue_token.is_empty());
3862 assert!(resp.from_serial.is_none());
3863 }
3864
3865 #[test]
3866 fn test_version_current_no_latest_tag() {
3867 let json = r#"{
3869 "dataset_id": 100, "current_serial": 5, "tags": []
3870 }"#;
3871 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3872 assert!(resp.latest_tag.is_none());
3873 assert!(resp.tags.is_empty());
3874 assert_eq!(resp.current_serial, 5);
3875 }
3876
3877 #[test]
3878 fn test_version_current_with_latest_tag() {
3879 let json = r#"{
3880 "dataset_id": 100, "current_serial": 42,
3881 "latest_tag": {
3882 "id": 1, "dataset_id": 100, "name": "v1.0", "serial": 42,
3883 "description": "test", "created_by": "user",
3884 "created_at": "2025-01-01T00:00:00Z",
3885 "image_count": 10, "label_count": 2, "annotation_set_count": 1
3886 },
3887 "tags": []
3888 }"#;
3889 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3890 assert!(resp.latest_tag.is_some());
3891 assert_eq!(resp.latest_tag.unwrap().name(), "v1.0");
3892 }
3893
3894 #[test]
3895 fn test_version_tag_is_current_field() {
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": true
3900 }"#;
3901 let tag: VersionTag = serde_json::from_str(json).unwrap();
3902 assert!(tag.is_current());
3903 }
3904
3905 #[test]
3906 fn test_version_tag_is_current_false() {
3907 let json = r#"{
3908 "id": 1, "dataset_id": 5, "name": "v1.0", "serial": 10,
3909 "created_by": "alice", "created_at": "2026-01-01T00:00:00Z",
3910 "is_current": false
3911 }"#;
3912 let tag: VersionTag = serde_json::from_str(json).unwrap();
3913 assert!(!tag.is_current());
3914 }
3915
3916 #[test]
3917 fn test_dataset_summary_deserialize() {
3918 let json = r#"{
3919 "dataset_id": 100, "current_serial": 10,
3920 "image_count": 5000, "annotation_counts": {"box": 10000},
3921 "sensor_counts": {}, "label_count": 8,
3922 "annotation_set_count": 2, "last_updated": "2025-06-01T12:00:00Z"
3923 }"#;
3924 let summary: DatasetSummary = serde_json::from_str(json).unwrap();
3925 assert_eq!(summary.image_count(), 5000);
3926 assert_eq!(summary.label_count(), 8);
3927 assert_eq!(summary.annotation_counts().get("box"), Some(&10000));
3928 }
3929
3930 #[test]
3931 fn test_restore_result_deserialize() {
3932 let json = r#"{
3933 "success": true, "new_serial": 45,
3934 "restored_from": {"tag": "v1.0", "serial": 42},
3935 "restored_counts": {"images": 5000, "labels": 15, "annotation_sets": 3},
3936 "message": "Dataset restored to tag v1.0"
3937 }"#;
3938 let result: RestoreResult = serde_json::from_str(json).unwrap();
3939 assert!(result.success);
3940 assert_eq!(result.new_serial, 45);
3941 assert_eq!(result.restored_from.tag, "v1.0");
3942 assert_eq!(result.restored_from.serial, 42);
3943 assert_eq!(result.restored_counts.images, 5000);
3944 }
3945
3946 #[test]
3947 fn test_sample_delete_params_serializes_all_fields() {
3948 let params = SampleDeleteParams {
3954 dataset_id: 42,
3955 image_ids: vec![1, 2, 3],
3956 sequence_ids: Vec::new(),
3957 delete_all: false,
3958 };
3959 let value = serde_json::to_value(¶ms).unwrap();
3960 let obj = value.as_object().unwrap();
3961 assert_eq!(obj.len(), 4);
3962 assert_eq!(obj["dataset_id"], serde_json::json!(42));
3963 assert_eq!(obj["image_ids"], serde_json::json!([1, 2, 3]));
3964 assert_eq!(obj["sequence_ids"], serde_json::json!([]));
3965 assert_eq!(obj["delete_all"], serde_json::json!(false));
3966 }
3967}
3968
3969#[cfg(test)]
3970mod tests_task_data_list {
3971 use super::*;
3972
3973 #[test]
3974 fn task_data_list_deserializes_from_server_shape() {
3975 let json = r#"{
3976 "server": "test.edgefirst.studio",
3977 "organization_uid": "org-abc123",
3978 "traces": ["trace/imx95.json"],
3979 "data": {
3980 "predictions": ["predictions.parquet"],
3981 "trace": ["imx95.json"]
3982 }
3983 }"#;
3984 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
3985 assert_eq!(parsed.server, "test.edgefirst.studio");
3986 assert_eq!(parsed.organization_uid, "org-abc123");
3987 assert_eq!(parsed.traces, vec!["trace/imx95.json"]);
3988 assert_eq!(
3989 parsed.data.get("predictions").unwrap(),
3990 &vec!["predictions.parquet".to_string()]
3991 );
3992 }
3993}
3994
3995#[cfg(test)]
3996mod tests_upload_data {
3997 #[test]
4001 fn folder_empty_string_is_normalised() {
4002 let folder: Option<&str> = Some("");
4003 assert!(folder.filter(|s| !s.is_empty()).is_none());
4004
4005 let folder_real: Option<&str> = Some("predictions");
4006 assert!(folder_real.filter(|s| !s.is_empty()).is_some());
4007 }
4008}
4009
4010#[cfg(test)]
4011mod tests_job_struct {
4012 use super::*;
4013
4014 #[test]
4015 fn job_deserializes_with_all_fields() {
4016 let json = r#"{
4017 "code": "edgefirst-validator:2.9.5",
4018 "title": "EdgeFirst Validator",
4019 "job_name": "smoke-test",
4020 "job_id": "aws-batch-abc",
4021 "state": "RUNNING",
4022 "launch": "2026-05-14T15:00:00Z",
4023 "task_id": 6789
4024 }"#;
4025 let job: Job = serde_json::from_str(json).unwrap();
4026 assert_eq!(job.code, "edgefirst-validator:2.9.5");
4027 assert_eq!(job.title, "EdgeFirst Validator");
4028 assert_eq!(job.job_name, "smoke-test");
4029 assert_eq!(job.job_id, "aws-batch-abc");
4030 assert_eq!(job.state, "RUNNING");
4031 assert!(job.launch.is_some());
4032 assert_eq!(job.task_id, 6789);
4033 }
4034
4035 #[test]
4036 fn job_tolerates_missing_optional_fields() {
4037 let json = r#"{ "task_id": 42 }"#;
4041 let job: Job = serde_json::from_str(json).unwrap();
4042 assert_eq!(job.task_id, 42);
4043 assert!(job.code.is_empty());
4044 assert!(job.title.is_empty());
4045 assert!(job.job_name.is_empty());
4046 assert!(job.job_id.is_empty());
4047 assert!(job.state.is_empty());
4048 assert!(job.launch.is_none());
4049 }
4050
4051 #[test]
4052 fn job_task_id_accessor_saturates_negative_to_zero() {
4053 let job = Job {
4058 code: String::new(),
4059 title: String::new(),
4060 job_name: String::new(),
4061 job_id: String::new(),
4062 state: String::new(),
4063 launch: None,
4064 task_id: -1,
4065 };
4066 assert_eq!(job.task_id().value(), 0);
4067 }
4068
4069 #[test]
4070 fn job_task_id_accessor_passes_through_positive_values() {
4071 let job = Job {
4072 code: String::new(),
4073 title: String::new(),
4074 job_name: String::new(),
4075 job_id: String::new(),
4076 state: String::new(),
4077 launch: None,
4078 task_id: 12345,
4079 };
4080 assert_eq!(job.task_id().value(), 12345);
4081 }
4082
4083 #[test]
4084 fn job_ignores_unknown_fields() {
4085 let json = r#"{
4089 "code": "x",
4090 "task_id": 1,
4091 "docker_task": { "image": "x" },
4092 "aws_region": "us-east-1",
4093 "tags": ["a", "b"]
4094 }"#;
4095 let job: Job = serde_json::from_str(json).unwrap();
4096 assert_eq!(job.task_id, 1);
4097 }
4098}
4099
4100#[cfg(test)]
4101mod tests_task_info_schema_tolerance {
4102 use super::*;
4103
4104 #[test]
4109 fn task_info_accepts_task_description_field() {
4110 let json = r#"{
4112 "id": 6699,
4113 "type": "edgefirst-validator:2.9.5",
4114 "task_description": "Profiler run for IMX95",
4115 "status": "running"
4116 }"#;
4117 let info: TaskInfo = serde_json::from_str(json).unwrap();
4118 assert_eq!(info.description(), "Profiler run for IMX95");
4119 }
4120
4121 #[test]
4122 fn task_info_accepts_legacy_description_field() {
4123 let json = r#"{
4125 "id": 6699,
4126 "type": "edgefirst-validator:2.9.5",
4127 "description": "Legacy description"
4128 }"#;
4129 let info: TaskInfo = serde_json::from_str(json).unwrap();
4130 assert_eq!(info.description(), "Legacy description");
4131 }
4132
4133 #[test]
4134 fn task_info_tolerates_missing_description() {
4135 let json = r#"{
4137 "id": 6699,
4138 "type": "x"
4139 }"#;
4140 let info: TaskInfo = serde_json::from_str(json).unwrap();
4141 assert!(info.description().is_empty());
4142 }
4143
4144 #[test]
4145 fn task_info_tolerates_missing_dates_via_default() {
4146 let json = r#"{
4148 "id": 6699,
4149 "type": "x"
4150 }"#;
4151 let info: TaskInfo = serde_json::from_str(json).unwrap();
4152 assert_eq!(info.id().value(), 6699);
4154 }
4155
4156 #[test]
4157 fn task_info_status_accessor_returns_option() {
4158 let json = r#"{
4159 "id": 1,
4160 "type": "x"
4161 }"#;
4162 let info: TaskInfo = serde_json::from_str(json).unwrap();
4163 assert!(info.status().is_none());
4164 }
4165
4166 #[test]
4167 fn task_info_stages_returns_empty_map_when_unset() {
4168 let json = r#"{
4169 "id": 1,
4170 "type": "x"
4171 }"#;
4172 let info: TaskInfo = serde_json::from_str(json).unwrap();
4173 let stages = info.stages();
4174 assert!(stages.is_empty());
4175 }
4176}
4177
4178#[cfg(test)]
4179mod tests_stage_struct {
4180 use super::*;
4181
4182 #[test]
4183 fn stage_new_sets_only_supplied_fields() {
4184 let stage = Stage::new(
4185 None,
4186 "download".into(),
4187 Some("running".into()),
4188 Some("fetching".into()),
4189 42,
4190 );
4191 assert!(stage.task_id().is_none());
4192 assert_eq!(stage.stage(), "download");
4193 assert_eq!(stage.status().as_deref(), Some("running"));
4194 assert_eq!(stage.message().as_deref(), Some("fetching"));
4195 assert_eq!(stage.percentage(), 42);
4196 assert!(stage.description().is_none());
4198 }
4199
4200 #[test]
4201 fn stage_serializes_without_optional_none_fields() {
4202 let stage = Stage::new(None, "init".into(), None, None, 0);
4204 let json = serde_json::to_value(&stage).unwrap();
4205 assert!(json.get("status").is_none(), "got: {json}");
4206 assert!(json.get("message").is_none(), "got: {json}");
4207 assert!(json.get("docker_task_id").is_none(), "got: {json}");
4208 assert_eq!(json["stage"], "init");
4210 assert_eq!(json["percentage"], 0);
4211 }
4212
4213 #[test]
4214 fn stage_serializes_task_id_when_present() {
4215 let task_id = TaskID::from(0xdeadu64);
4216 let stage = Stage::new(Some(task_id), "x".into(), None, None, 0);
4217 let json = serde_json::to_value(&stage).unwrap();
4218 assert!(json.get("docker_task_id").is_some());
4221 }
4222
4223 #[test]
4224 fn stage_round_trips_through_json() {
4225 let stage = Stage::new(
4226 None,
4227 "train".into(),
4228 Some("done".into()),
4229 Some("epoch 100".into()),
4230 100,
4231 );
4232 let s = serde_json::to_string(&stage).unwrap();
4233 let back: Stage = serde_json::from_str(&s).unwrap();
4234 assert_eq!(back.stage(), "train");
4235 assert_eq!(back.status().as_deref(), Some("done"));
4236 assert_eq!(back.message().as_deref(), Some("epoch 100"));
4237 assert_eq!(back.percentage(), 100);
4238 }
4239}
4240
4241#[cfg(test)]
4242mod tests_task_data_list_extra {
4243 use super::*;
4244
4245 #[test]
4246 fn task_data_list_with_empty_data_map() {
4247 let json = r#"{
4248 "server": "studio",
4249 "organization_uid": "org-1",
4250 "traces": [],
4251 "data": {}
4252 }"#;
4253 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4254 assert!(parsed.traces.is_empty());
4255 assert!(parsed.data.is_empty());
4256 }
4257
4258 #[test]
4259 fn task_data_list_multiple_folders() {
4260 let json = r#"{
4261 "server": "studio",
4262 "organization_uid": "org-1",
4263 "traces": ["t1", "t2"],
4264 "data": {
4265 "predictions": ["a.parquet", "b.parquet"],
4266 "metrics": ["loss.json"]
4267 }
4268 }"#;
4269 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4270 assert_eq!(parsed.traces.len(), 2);
4271 assert_eq!(parsed.data.len(), 2);
4272 assert_eq!(parsed.data["predictions"].len(), 2);
4273 }
4274}
4275
4276#[cfg(test)]
4277mod tests_artifact_struct {
4278 use super::*;
4279
4280 #[test]
4281 fn artifact_accessors_return_strs() {
4282 let json = r#"{ "name": "best.onnx", "modelType": "yolo" }"#;
4285 let a: Artifact = serde_json::from_str(json).unwrap();
4286 assert_eq!(a.name(), "best.onnx");
4287 assert_eq!(a.model_type(), "yolo");
4288 }
4289}
4290
4291#[cfg(test)]
4292mod tests_task_status_serialize {
4293 use super::*;
4294
4295 #[test]
4296 fn task_status_uses_docker_task_id_wire_field() {
4297 let s = TaskStatus {
4298 task_id: TaskID::from(0x1a2bu64),
4299 status: "training".into(),
4300 };
4301 let json = serde_json::to_value(&s).unwrap();
4302 assert!(json.get("docker_task_id").is_some(), "got: {json}");
4304 assert_eq!(json["status"], "training");
4305 }
4306}
4307
4308#[cfg(test)]
4309mod tests_task_stages_serialize {
4310 use super::*;
4311
4312 #[test]
4313 fn task_stages_omits_empty_vec() {
4314 let stages = TaskStages {
4315 task_id: TaskID::from(1u64),
4316 stages: Vec::new(),
4317 };
4318 let json = serde_json::to_value(&stages).unwrap();
4319 assert!(json.get("stages").is_none(), "got: {json}");
4321 }
4322
4323 #[test]
4324 fn task_stages_serializes_non_empty_vec() {
4325 let stages = TaskStages {
4326 task_id: TaskID::from(1u64),
4327 stages: vec![std::collections::HashMap::from([(
4328 "stage".to_string(),
4329 "download".to_string(),
4330 )])],
4331 };
4332 let json = serde_json::to_value(&stages).unwrap();
4333 assert_eq!(json["stages"][0]["stage"], "download");
4334 }
4335}