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")]
197 credits: i64,
198}
199
200impl Display for Organization {
201 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
202 write!(f, "{}", self.name())
203 }
204}
205
206impl Organization {
207 pub fn id(&self) -> OrganizationID {
208 self.id
209 }
210
211 pub fn name(&self) -> &str {
212 &self.name
213 }
214
215 pub fn credits(&self) -> i64 {
216 self.credits
217 }
218}
219
220#[derive(Deserialize, Clone, Debug)]
226pub struct UsageSummary {
227 #[serde(default)]
228 credits: f64,
229 #[serde(default)]
230 funds: f64,
231 #[serde(default, rename = "total_funds_and_credits")]
232 total: f64,
233}
234
235impl UsageSummary {
236 pub fn credits(&self) -> f64 {
237 self.credits
238 }
239
240 pub fn funds(&self) -> f64 {
241 self.funds
242 }
243
244 pub fn total(&self) -> f64 {
245 self.total
246 }
247}
248
249typeid!(
250 ProjectID,
271 "p"
272);
273
274typeid!(
275 ExperimentID,
296 "exp"
297);
298
299typeid!(
300 TrainingSessionID,
321 "t"
322);
323
324typeid!(
325 ValidationSessionID,
345 "v"
346);
347
348typeid!(
349 SnapshotID,
365 "ss"
366);
367
368typeid!(
369 TaskID,
385 "task"
386);
387
388typeid!(
389 DatasetID,
410 "ds"
411);
412
413typeid!(
414 AnnotationSetID,
430 "as"
431);
432
433typeid!(
434 SampleID,
450 "s"
451);
452
453typeid!(
454 AppId,
460 "app"
461);
462
463typeid!(
464 ImageId,
470 "im"
471);
472
473typeid!(
474 SequenceId,
480 "se"
481);
482
483#[derive(Deserialize, Clone, Debug)]
487pub struct Project {
488 id: ProjectID,
489 name: String,
490 description: String,
491}
492
493impl Display for Project {
494 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
495 write!(f, "{} {}", self.id(), self.name())
496 }
497}
498
499impl Project {
500 pub fn id(&self) -> ProjectID {
501 self.id
502 }
503
504 pub fn name(&self) -> &str {
505 &self.name
506 }
507
508 pub fn description(&self) -> &str {
509 &self.description
510 }
511
512 pub async fn datasets(
513 &self,
514 client: &client::Client,
515 name: Option<&str>,
516 ) -> Result<Vec<Dataset>, Error> {
517 client.datasets(self.id, name).await
518 }
519
520 pub async fn experiments(
521 &self,
522 client: &client::Client,
523 name: Option<&str>,
524 ) -> Result<Vec<Experiment>, Error> {
525 client.experiments(self.id, name).await
526 }
527}
528
529#[derive(Deserialize, Debug)]
530pub struct SamplesCountResult {
531 pub total: u64,
532}
533
534#[derive(Serialize, Clone, Debug)]
535pub struct SamplesListParams {
536 pub dataset_id: DatasetID,
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub annotation_set_id: Option<AnnotationSetID>,
539 #[serde(skip_serializing_if = "Option::is_none")]
540 pub continue_token: Option<String>,
541 #[serde(skip_serializing_if = "Vec::is_empty")]
542 pub types: Vec<String>,
543 #[serde(skip_serializing_if = "Vec::is_empty")]
544 pub group_names: Vec<String>,
545 #[serde(skip_serializing_if = "Option::is_none")]
546 pub tag: Option<String>,
547 #[serde(skip_serializing_if = "Option::is_none")]
551 pub limit: Option<u32>,
552}
553
554#[derive(Deserialize, Debug)]
555pub struct SamplesListResult {
556 pub samples: Vec<Sample>,
557 pub continue_token: Option<String>,
558}
559
560#[derive(Serialize, Clone, Debug)]
562pub struct SampleDimensionUpdate {
563 pub id: SampleID,
564 pub width: u32,
565 pub height: u32,
566}
567
568#[derive(Serialize, Clone, Debug)]
570pub struct SamplesUpdateDimensionsParams {
571 pub dataset_id: DatasetID,
572 pub samples: Vec<SampleDimensionUpdate>,
573}
574
575#[derive(Deserialize, Debug)]
577pub struct SamplesUpdateDimensionsResult {
578 pub updated: u64,
579}
580
581#[derive(Serialize, Clone, Debug)]
586pub struct SamplesPopulateParams {
587 pub dataset_id: DatasetID,
588 #[serde(skip_serializing_if = "Option::is_none")]
589 pub annotation_set_id: Option<AnnotationSetID>,
590 #[serde(skip_serializing_if = "Option::is_none")]
591 pub presigned_urls: Option<bool>,
592 pub samples: Vec<Sample>,
593}
594
595#[derive(Deserialize, Debug, Clone)]
601pub struct SamplesPopulateResult {
602 pub uuid: String,
604 pub urls: Vec<PresignedUrl>,
606}
607
608#[derive(Deserialize, Debug, Clone)]
610pub struct PresignedUrl {
611 pub filename: String,
613 pub key: String,
615 pub url: String,
617}
618
619#[derive(Serialize, Clone, Debug)]
632pub struct ServerAnnotation {
633 #[serde(skip_serializing_if = "Option::is_none")]
635 pub label_id: Option<u64>,
636 #[serde(skip_serializing_if = "Option::is_none")]
638 pub label_index: Option<u64>,
639 #[serde(skip_serializing_if = "Option::is_none")]
641 pub label_name: Option<String>,
642 #[serde(rename = "type")]
644 pub annotation_type: String,
645 pub x: f64,
647 pub y: f64,
649 pub w: f64,
651 pub h: f64,
653 pub score: f64,
655 #[serde(skip_serializing_if = "String::is_empty")]
657 pub polygon: String,
658 pub image_id: u64,
660 pub annotation_set_id: u64,
662 #[serde(skip_serializing_if = "Option::is_none")]
664 pub object_reference: Option<String>,
665}
666
667#[derive(Serialize, Debug)]
669pub struct AnnotationAddBulkParams {
670 pub annotation_set_id: u64,
671 pub annotations: Vec<ServerAnnotation>,
672}
673
674#[derive(Serialize, Debug)]
676pub struct AnnotationBulkDeleteParams {
677 pub annotation_set_id: u64,
678 pub annotation_types: Vec<String>,
679 #[serde(skip_serializing_if = "Vec::is_empty")]
681 pub image_ids: Vec<u64>,
682 #[serde(skip_serializing_if = "Option::is_none")]
684 pub delete_all: Option<bool>,
685}
686
687#[derive(Serialize, Debug)]
695pub struct SampleDeleteParams {
696 pub dataset_id: u64,
697 pub image_ids: Vec<u64>,
698 pub sequence_ids: Vec<i64>,
699 pub delete_all: bool,
700}
701
702#[derive(Deserialize)]
703pub struct Snapshot {
704 id: SnapshotID,
705 description: String,
706 status: String,
707 path: String,
708 #[serde(rename = "date")]
709 created: DateTime<Utc>,
710}
711
712impl Display for Snapshot {
713 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
714 write!(f, "{} {}", self.id, self.description)
715 }
716}
717
718impl Snapshot {
719 pub fn id(&self) -> SnapshotID {
720 self.id
721 }
722
723 pub fn description(&self) -> &str {
724 &self.description
725 }
726
727 pub fn status(&self) -> &str {
728 &self.status
729 }
730
731 pub fn path(&self) -> &str {
732 &self.path
733 }
734
735 pub fn created(&self) -> &DateTime<Utc> {
736 &self.created
737 }
738}
739
740#[derive(Serialize, Debug)]
741pub struct SnapshotRestore {
742 pub project_id: ProjectID,
743 pub snapshot_id: SnapshotID,
744 pub fps: u64,
745 #[serde(rename = "enabled_topics", skip_serializing_if = "Vec::is_empty")]
746 pub topics: Vec<String>,
747 #[serde(rename = "label_names", skip_serializing_if = "Vec::is_empty")]
748 pub autolabel: Vec<String>,
749 #[serde(rename = "depth_gen")]
750 pub autodepth: bool,
751 pub agtg_pipeline: bool,
752 #[serde(skip_serializing_if = "Option::is_none")]
753 pub dataset_name: Option<String>,
754 #[serde(skip_serializing_if = "Option::is_none")]
755 pub dataset_description: Option<String>,
756}
757
758#[derive(Deserialize, Debug)]
759pub struct SnapshotRestoreResult {
760 pub id: SnapshotID,
761 pub description: String,
762 pub dataset_name: String,
763 pub dataset_id: DatasetID,
764 pub annotation_set_id: AnnotationSetID,
765 #[serde(default)]
766 pub task_id: Option<TaskID>,
767 #[serde(default)]
771 pub date: Option<DateTime<Utc>>,
772}
773
774#[derive(Serialize, Debug)]
779pub struct SnapshotCreateFromDataset {
780 pub description: String,
782 pub dataset_id: DatasetID,
784 pub annotation_set_id: AnnotationSetID,
786}
787
788#[derive(Deserialize, Debug)]
792pub struct SnapshotFromDatasetResult {
793 #[serde(alias = "snapshot_id")]
795 pub id: SnapshotID,
796 #[serde(default)]
798 pub task_id: Option<TaskID>,
799}
800
801#[derive(Deserialize)]
802pub struct Experiment {
803 id: ExperimentID,
804 project_id: ProjectID,
805 name: String,
806 description: String,
807}
808
809impl Display for Experiment {
810 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
811 write!(f, "{} {}", self.id, self.name)
812 }
813}
814
815impl Experiment {
816 pub fn id(&self) -> ExperimentID {
817 self.id
818 }
819
820 pub fn project_id(&self) -> ProjectID {
821 self.project_id
822 }
823
824 pub fn name(&self) -> &str {
825 &self.name
826 }
827
828 pub fn description(&self) -> &str {
829 &self.description
830 }
831
832 pub async fn project(&self, client: &client::Client) -> Result<Project, Error> {
833 client.project(self.project_id).await
834 }
835
836 pub async fn training_sessions(
837 &self,
838 client: &client::Client,
839 name: Option<&str>,
840 ) -> Result<Vec<TrainingSession>, Error> {
841 client.training_sessions(self.id, name).await
842 }
843}
844
845#[derive(Serialize, Debug)]
846pub struct PublishMetrics {
847 #[serde(rename = "trainer_session_id", skip_serializing_if = "Option::is_none")]
848 pub trainer_session_id: Option<TrainingSessionID>,
849 #[serde(
850 rename = "validate_session_id",
851 skip_serializing_if = "Option::is_none"
852 )]
853 pub validate_session_id: Option<ValidationSessionID>,
854 pub metrics: HashMap<String, Parameter>,
855}
856
857#[derive(Deserialize)]
858struct TrainingSessionParams {
859 #[serde(default)]
860 model_params: HashMap<String, Parameter>,
861 #[serde(default)]
862 dataset_params: DatasetParams,
863}
864
865#[derive(Deserialize)]
866pub struct TrainingSession {
867 id: TrainingSessionID,
868 #[serde(rename = "trainer_id")]
869 experiment_id: ExperimentID,
870 model: String,
871 name: String,
872 description: String,
873 params: TrainingSessionParams,
874 #[serde(rename = "docker_task")]
875 task: Task,
876}
877
878impl Display for TrainingSession {
879 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
880 write!(f, "{} {}", self.id, self.name())
881 }
882}
883
884impl TrainingSession {
885 pub fn id(&self) -> TrainingSessionID {
886 self.id
887 }
888
889 pub fn name(&self) -> &str {
890 &self.name
891 }
892
893 pub fn description(&self) -> &str {
894 &self.description
895 }
896
897 pub fn model(&self) -> &str {
898 &self.model
899 }
900
901 pub fn experiment_id(&self) -> ExperimentID {
902 self.experiment_id
903 }
904
905 pub fn task(&self) -> Task {
906 self.task.clone()
907 }
908
909 pub fn model_params(&self) -> &HashMap<String, Parameter> {
910 &self.params.model_params
911 }
912
913 pub fn dataset_params(&self) -> &DatasetParams {
914 &self.params.dataset_params
915 }
916
917 pub fn train_group(&self) -> &str {
918 &self.params.dataset_params.train_group
919 }
920
921 pub fn val_group(&self) -> &str {
922 &self.params.dataset_params.val_group
923 }
924
925 pub async fn experiment(&self, client: &client::Client) -> Result<Experiment, Error> {
926 client.experiment(self.experiment_id).await
927 }
928
929 pub async fn dataset(&self, client: &client::Client) -> Result<Dataset, Error> {
930 if self.params.dataset_params.dataset_id.value() == 0 {
931 return Err(Error::InvalidParameters(
932 "training session has no dataset configured".into(),
933 ));
934 }
935 client.dataset(self.params.dataset_params.dataset_id).await
936 }
937
938 pub async fn annotation_set(&self, client: &client::Client) -> Result<AnnotationSet, Error> {
939 if self.params.dataset_params.annotation_set_id.value() == 0 {
940 return Err(Error::InvalidParameters(
941 "training session has no annotation set configured".into(),
942 ));
943 }
944 client
945 .annotation_set(self.params.dataset_params.annotation_set_id)
946 .await
947 }
948
949 pub async fn artifacts(&self, client: &client::Client) -> Result<Vec<Artifact>, Error> {
950 client.artifacts(self.id).await
951 }
952
953 pub async fn metrics(
954 &self,
955 client: &client::Client,
956 ) -> Result<HashMap<String, Parameter>, Error> {
957 #[derive(Deserialize)]
958 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
959 enum Response {
960 Empty {},
961 Map(HashMap<String, Parameter>),
962 String(String),
963 }
964
965 let params = HashMap::from([("trainer_session_id", self.id().value())]);
966 let resp: Response = client
967 .rpc("trainer.session.metrics".to_owned(), Some(params))
968 .await?;
969
970 Ok(match resp {
971 Response::String(metrics) => serde_json::from_str(&metrics)?,
972 Response::Map(metrics) => metrics,
973 Response::Empty {} => HashMap::new(),
974 })
975 }
976
977 pub async fn set_metrics(
978 &self,
979 client: &client::Client,
980 metrics: HashMap<String, Parameter>,
981 ) -> Result<(), Error> {
982 let metrics = PublishMetrics {
983 trainer_session_id: Some(self.id()),
984 validate_session_id: None,
985 metrics,
986 };
987
988 let _: String = client
989 .rpc("trainer.session.metrics".to_owned(), Some(metrics))
990 .await?;
991
992 Ok(())
993 }
994
995 pub async fn download_artifact(
997 &self,
998 client: &client::Client,
999 filename: &str,
1000 ) -> Result<Vec<u8>, Error> {
1001 client
1002 .fetch(&format!(
1003 "download_model?training_session_id={}&file={}",
1004 self.id().value(),
1005 filename
1006 ))
1007 .await
1008 }
1009
1010 pub async fn upload_artifact(
1014 &self,
1015 client: &client::Client,
1016 filename: &str,
1017 path: PathBuf,
1018 ) -> Result<(), Error> {
1019 self.upload(client, &[(format!("artifacts/{}", filename), path)])
1020 .await
1021 }
1022
1023 pub async fn download_checkpoint(
1025 &self,
1026 client: &client::Client,
1027 filename: &str,
1028 ) -> Result<Vec<u8>, Error> {
1029 client
1030 .fetch(&format!(
1031 "download_checkpoint?folder=checkpoints&training_session_id={}&file={}",
1032 self.id().value(),
1033 filename
1034 ))
1035 .await
1036 }
1037
1038 pub async fn upload_checkpoint(
1042 &self,
1043 client: &client::Client,
1044 filename: &str,
1045 path: PathBuf,
1046 ) -> Result<(), Error> {
1047 self.upload(client, &[(format!("checkpoints/{}", filename), path)])
1048 .await
1049 }
1050
1051 pub async fn download(&self, client: &client::Client, filename: &str) -> Result<String, Error> {
1055 #[derive(Serialize)]
1056 struct DownloadRequest {
1057 session_id: TrainingSessionID,
1058 file_path: String,
1059 }
1060
1061 let params = DownloadRequest {
1062 session_id: self.id(),
1063 file_path: filename.to_string(),
1064 };
1065
1066 client
1067 .rpc("trainer.download.file".to_owned(), Some(params))
1068 .await
1069 }
1070
1071 pub async fn upload(
1072 &self,
1073 client: &client::Client,
1074 files: &[(String, PathBuf)],
1075 ) -> Result<(), Error> {
1076 let mut parts = Form::new().part(
1077 "params",
1078 Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1079 );
1080
1081 for (name, path) in files {
1082 let file_part = Part::file(path).await?.file_name(name.to_owned());
1083 parts = parts.part("file", file_part);
1084 }
1085
1086 let result = client.post_multipart("trainer.upload.files", parts).await?;
1087 trace!("TrainingSession::upload: {:?}", result);
1088 Ok(())
1089 }
1090}
1091
1092#[derive(Deserialize, Clone, Debug)]
1093pub struct ValidationSession {
1094 id: ValidationSessionID,
1095 description: String,
1096 dataset_id: DatasetID,
1097 experiment_id: ExperimentID,
1098 training_session_id: TrainingSessionID,
1099 #[serde(rename = "gt_annotation_set_id")]
1100 annotation_set_id: AnnotationSetID,
1101 #[serde(deserialize_with = "validation_session_params")]
1102 params: HashMap<String, Parameter>,
1103 #[serde(rename = "docker_task")]
1104 task: Task,
1105}
1106
1107fn validation_session_params<'de, D>(
1108 deserializer: D,
1109) -> Result<HashMap<String, Parameter>, D::Error>
1110where
1111 D: Deserializer<'de>,
1112{
1113 #[derive(Deserialize)]
1114 struct ModelParams {
1115 validation: Option<HashMap<String, Parameter>>,
1116 }
1117
1118 #[derive(Deserialize)]
1119 struct ValidateParams {
1120 model: String,
1121 }
1122
1123 #[derive(Deserialize)]
1124 struct Params {
1125 model_params: ModelParams,
1126 validate_params: ValidateParams,
1127 }
1128
1129 let params = Params::deserialize(deserializer)?;
1130 let params = match params.model_params.validation {
1131 Some(mut map) => {
1132 map.insert(
1133 "model".to_string(),
1134 Parameter::String(params.validate_params.model),
1135 );
1136 map
1137 }
1138 None => HashMap::from([(
1139 "model".to_string(),
1140 Parameter::String(params.validate_params.model),
1141 )]),
1142 };
1143
1144 Ok(params)
1145}
1146
1147impl ValidationSession {
1148 pub fn id(&self) -> ValidationSessionID {
1149 self.id
1150 }
1151
1152 pub fn name(&self) -> &str {
1153 self.task.name()
1154 }
1155
1156 pub fn description(&self) -> &str {
1157 &self.description
1158 }
1159
1160 pub fn dataset_id(&self) -> DatasetID {
1161 self.dataset_id
1162 }
1163
1164 pub fn experiment_id(&self) -> ExperimentID {
1165 self.experiment_id
1166 }
1167
1168 pub fn training_session_id(&self) -> TrainingSessionID {
1169 self.training_session_id
1170 }
1171
1172 pub fn annotation_set_id(&self) -> AnnotationSetID {
1173 self.annotation_set_id
1174 }
1175
1176 pub fn params(&self) -> &HashMap<String, Parameter> {
1177 &self.params
1178 }
1179
1180 pub fn task(&self) -> &Task {
1181 &self.task
1182 }
1183
1184 pub async fn metrics(
1185 &self,
1186 client: &client::Client,
1187 ) -> Result<HashMap<String, Parameter>, Error> {
1188 #[derive(Deserialize)]
1189 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1190 enum Response {
1191 Empty {},
1192 Map(HashMap<String, Parameter>),
1193 String(String),
1194 }
1195
1196 let params = HashMap::from([("validate_session_id", self.id().value())]);
1197 let resp: Response = client
1198 .rpc("validate.session.metrics".to_owned(), Some(params))
1199 .await?;
1200
1201 Ok(match resp {
1202 Response::String(metrics) => serde_json::from_str(&metrics)?,
1203 Response::Map(metrics) => metrics,
1204 Response::Empty {} => HashMap::new(),
1205 })
1206 }
1207
1208 pub async fn set_metrics(
1209 &self,
1210 client: &client::Client,
1211 metrics: HashMap<String, Parameter>,
1212 ) -> Result<(), Error> {
1213 let metrics = PublishMetrics {
1214 trainer_session_id: None,
1215 validate_session_id: Some(self.id()),
1216 metrics,
1217 };
1218
1219 let _: String = client
1220 .rpc("validate.session.metrics".to_owned(), Some(metrics))
1221 .await?;
1222
1223 Ok(())
1224 }
1225
1226 pub async fn upload_data(
1251 &self,
1252 client: &client::Client,
1253 files: &[(String, std::path::PathBuf)],
1254 folder: Option<&str>,
1255 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1256 ) -> Result<(), Error> {
1257 use futures::StreamExt;
1258 use std::sync::{
1259 Arc,
1260 atomic::{AtomicUsize, Ordering},
1261 };
1262 use tokio_util::io::ReaderStream;
1263
1264 let mut total: usize = 0;
1266 let mut file_meta = Vec::with_capacity(files.len());
1267 for (name, path) in files {
1268 let f = tokio::fs::File::open(path).await?;
1269 let len = f.metadata().await?.len() as usize;
1270 total += len;
1271 file_meta.push((name.clone(), f, len));
1272 }
1273
1274 let sent = Arc::new(AtomicUsize::new(0));
1276
1277 let mut form = Form::new().text("session_id", self.id().value().to_string());
1278 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
1279 form = form.text("folder", folder.to_owned());
1280 }
1281
1282 for (name, file, len) in file_meta {
1283 let reader_stream = ReaderStream::new(file);
1284 let sent_clone = sent.clone();
1285 let progress_clone = progress.clone();
1286 let progress_stream = reader_stream.inspect(move |chunk_result| {
1287 if let Ok(chunk) = chunk_result {
1288 let current =
1289 sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1290 if let Some(tx) = &progress_clone {
1295 let _ = tx.try_send(Progress {
1296 current,
1297 total,
1298 status: None,
1299 });
1300 }
1301 }
1302 });
1303 let body = reqwest::Body::wrap_stream(progress_stream);
1304 let part = Part::stream_with_length(body, len as u64).file_name(name);
1305 form = form.part("file", part);
1306 }
1307
1308 let result = match client.post_multipart("val.data.upload", form).await {
1309 Ok(_) => Ok(()),
1310 Err(Error::RpcError(code, msg)) => {
1311 Err(client::map_rpc_error("val.data.upload", code, msg, None))
1312 }
1313 Err(e) => Err(e),
1314 };
1315
1316 if result.is_ok()
1321 && let Some(tx) = progress
1322 {
1323 let _ = tx
1324 .send(Progress {
1325 current: total,
1326 total,
1327 status: None,
1328 })
1329 .await;
1330 }
1331 result
1332 }
1333
1334 pub async fn download_data(
1354 &self,
1355 client: &client::Client,
1356 filename: &str,
1357 output_path: &std::path::Path,
1358 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1359 ) -> Result<(), Error> {
1360 let req = client::ValDataDownloadRequest {
1361 session_id: self.id().value(),
1362 filename: filename.to_owned(),
1363 };
1364 match client
1365 .rpc_download("val.data.download", &req, output_path, progress)
1366 .await
1367 {
1368 Ok(()) => Ok(()),
1369 Err(Error::RpcError(code, msg)) => {
1370 Err(client::map_rpc_error("val.data.download", code, msg, None))
1371 }
1372 Err(e) => Err(e),
1373 }
1374 }
1375
1376 pub async fn data_list(&self, client: &client::Client) -> Result<Vec<String>, Error> {
1391 let req = client::ValDataListRequest {
1392 session_id: self.id().value(),
1393 };
1394 match client.rpc("val.data.list".to_owned(), Some(&req)).await {
1395 Ok(r) => Ok(r),
1396 Err(Error::RpcError(code, msg)) => {
1397 Err(client::map_rpc_error("val.data.list", code, msg, None))
1398 }
1399 Err(e) => Err(e),
1400 }
1401 }
1402}
1403
1404#[derive(Debug, Clone)]
1423pub struct StartValidationRequest {
1424 pub project_id: ProjectID,
1425 pub name: String,
1426 pub training_session_id: TrainingSessionID,
1427 pub model_file: String,
1428 pub val_type: String,
1429 pub params: HashMap<String, Parameter>,
1430 pub is_local: bool,
1431 pub is_kubernetes: bool,
1432 pub description: Option<String>,
1433 pub dataset_id: Option<DatasetID>,
1434 pub annotation_set_id: Option<AnnotationSetID>,
1435 pub snapshot_id: Option<SnapshotID>,
1436}
1437
1438#[derive(Deserialize, Debug, Clone)]
1453pub struct NewValidationSession {
1454 #[serde(rename = "id")]
1455 pub task_id: TaskID,
1456 #[serde(rename = "val_session_id", default)]
1457 pub session_id: Option<ValidationSessionID>,
1458}
1459
1460impl Display for NewValidationSession {
1461 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1462 match self.session_id {
1463 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1464 None => write!(f, "task {} (no session)", self.task_id),
1465 }
1466 }
1467}
1468
1469#[derive(Debug, Clone)]
1489pub struct StartTrainingRequest {
1490 pub project_id: ProjectID,
1492 pub name: String,
1494 pub experiment_id: ExperimentID,
1496 pub trainer_type: String,
1499 pub dataset_id: DatasetID,
1501 pub annotation_set_id: AnnotationSetID,
1503 pub tag_name: Option<String>,
1507 pub train_group: Option<String>,
1509 pub val_group: Option<String>,
1511 pub session_name: Option<String>,
1514 pub session_description: Option<String>,
1516 pub weights_session: Option<TrainingSessionID>,
1518 pub params: HashMap<String, Parameter>,
1520 pub is_local: bool,
1522 pub is_kubernetes: bool,
1524}
1525
1526#[derive(Deserialize, Debug, Clone)]
1539pub struct NewTrainingSession {
1540 #[serde(rename = "id")]
1541 pub task_id: TaskID,
1542 #[serde(rename = "train_session_id", default)]
1543 pub session_id: Option<TrainingSessionID>,
1544}
1545
1546impl Display for NewTrainingSession {
1547 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1548 match self.session_id {
1549 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1550 None => write!(f, "task {} (no session)", self.task_id),
1551 }
1552 }
1553}
1554
1555#[derive(Deserialize, Debug, Clone)]
1563pub struct Tag {
1564 pub id: u64,
1566 pub name: String,
1568 #[serde(default)]
1570 pub dataset_id: u64,
1571}
1572
1573#[derive(Deserialize, Clone, Debug, Default)]
1584#[serde(default)]
1585pub struct DatasetParams {
1586 dataset_id: DatasetID,
1587 annotation_set_id: AnnotationSetID,
1588 #[serde(rename = "train_group_name")]
1589 train_group: String,
1590 #[serde(rename = "val_group_name")]
1591 val_group: String,
1592}
1593
1594impl DatasetParams {
1595 pub fn dataset_id(&self) -> DatasetID {
1596 self.dataset_id
1597 }
1598
1599 pub fn annotation_set_id(&self) -> AnnotationSetID {
1600 self.annotation_set_id
1601 }
1602
1603 pub fn train_group(&self) -> &str {
1604 &self.train_group
1605 }
1606
1607 pub fn val_group(&self) -> &str {
1608 &self.val_group
1609 }
1610}
1611
1612#[derive(Serialize, Debug, Clone)]
1613pub struct TasksListParams {
1614 #[serde(skip_serializing_if = "Option::is_none")]
1615 pub continue_token: Option<String>,
1616 #[serde(skip_serializing_if = "Option::is_none")]
1617 pub types: Option<Vec<String>>,
1618 #[serde(rename = "manage_types", skip_serializing_if = "Option::is_none")]
1619 pub manager: Option<Vec<String>>,
1620 #[serde(skip_serializing_if = "Option::is_none")]
1621 pub status: Option<Vec<String>>,
1622}
1623
1624#[derive(Debug, Clone, Serialize, Deserialize)]
1630pub struct TaskDataList {
1631 pub server: String,
1632 #[serde(rename = "organization_uid")]
1633 pub organization_uid: String,
1634 #[serde(default)]
1635 pub traces: Vec<String>,
1636 #[serde(default)]
1637 pub data: std::collections::HashMap<String, Vec<String>>,
1638}
1639
1640#[derive(Debug, Clone, Serialize, Deserialize)]
1645pub struct Job {
1646 #[serde(default)]
1648 pub code: String,
1649 #[serde(default)]
1651 pub title: String,
1652 #[serde(default)]
1654 pub job_name: String,
1655 #[serde(default)]
1657 pub job_id: String,
1658 #[serde(default)]
1660 pub state: String,
1661 #[serde(default)]
1663 pub launch: Option<DateTime<Utc>>,
1664 pub task_id: i64,
1669}
1670
1671impl Job {
1672 pub fn task_id(&self) -> TaskID {
1678 TaskID::from(self.task_id.max(0) as u64)
1679 }
1680}
1681
1682#[derive(Deserialize, Debug, Clone)]
1683pub struct TasksListResult {
1684 pub tasks: Vec<Task>,
1685 pub continue_token: Option<String>,
1686}
1687
1688#[derive(Deserialize, Debug, Clone)]
1689pub struct Task {
1690 id: TaskID,
1691 name: String,
1692 #[serde(rename = "type")]
1693 workflow: String,
1694 status: String,
1695 #[serde(rename = "manage_type")]
1696 manager: Option<String>,
1697 #[serde(rename = "instance_type")]
1698 instance: String,
1699 #[serde(rename = "date")]
1700 created: DateTime<Utc>,
1701}
1702
1703impl Task {
1704 pub fn id(&self) -> TaskID {
1705 self.id
1706 }
1707
1708 pub fn name(&self) -> &str {
1709 &self.name
1710 }
1711
1712 pub fn workflow(&self) -> &str {
1713 &self.workflow
1714 }
1715
1716 pub fn status(&self) -> &str {
1717 &self.status
1718 }
1719
1720 pub fn manager(&self) -> Option<&str> {
1721 self.manager.as_deref()
1722 }
1723
1724 pub fn instance(&self) -> &str {
1725 &self.instance
1726 }
1727
1728 pub fn created(&self) -> &DateTime<Utc> {
1729 &self.created
1730 }
1731}
1732
1733impl Display for Task {
1734 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1735 write!(
1736 f,
1737 "{} [{:?} {}] {}",
1738 self.id,
1739 self.manager(),
1740 self.workflow(),
1741 self.name()
1742 )
1743 }
1744}
1745
1746#[derive(Deserialize, Debug, Clone)]
1747pub struct TaskInfo {
1748 id: TaskID,
1749 project_id: Option<ProjectID>,
1750 #[serde(rename = "task_description", alias = "description", default)]
1751 description: String,
1752 #[serde(rename = "type")]
1753 workflow: String,
1754 status: Option<String>,
1755 #[serde(default)]
1756 progress: TaskProgress,
1757 #[serde(
1758 rename = "created_date",
1759 alias = "created",
1760 default = "default_datetime_utc"
1761 )]
1762 created: DateTime<Utc>,
1763 #[serde(
1764 rename = "end_date",
1765 alias = "completed",
1766 default = "default_datetime_utc"
1767 )]
1768 completed: DateTime<Utc>,
1769}
1770
1771fn default_datetime_utc() -> DateTime<Utc> {
1772 DateTime::UNIX_EPOCH
1773}
1774
1775impl Display for TaskInfo {
1776 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1777 write!(f, "{} {}: {}", self.id, self.workflow(), self.description())
1778 }
1779}
1780
1781impl TaskInfo {
1782 pub fn id(&self) -> TaskID {
1783 self.id
1784 }
1785
1786 pub fn project_id(&self) -> Option<ProjectID> {
1787 self.project_id
1788 }
1789
1790 pub fn description(&self) -> &str {
1791 &self.description
1792 }
1793
1794 pub fn workflow(&self) -> &str {
1795 &self.workflow
1796 }
1797
1798 pub fn status(&self) -> &Option<String> {
1799 &self.status
1800 }
1801
1802 pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1803 let t = client.task_status(self.id(), status).await?;
1804 self.status = Some(t.status);
1805 Ok(())
1806 }
1807
1808 pub fn stages(&self) -> HashMap<String, Stage> {
1809 match &self.progress.stages {
1810 Some(stages) => stages.clone(),
1811 None => HashMap::new(),
1812 }
1813 }
1814
1815 pub async fn update_stage(
1816 &mut self,
1817 client: &Client,
1818 stage: &str,
1819 status: &str,
1820 message: &str,
1821 percentage: u8,
1822 ) -> Result<(), Error> {
1823 client
1824 .update_stage(self.id(), stage, status, message, percentage)
1825 .await?;
1826 let t = client.task_info(self.id()).await?;
1827 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1828 Ok(())
1829 }
1830
1831 pub async fn set_stages(
1832 &mut self,
1833 client: &Client,
1834 stages: &[(&str, &str)],
1835 ) -> Result<(), Error> {
1836 client.set_stages(self.id(), stages).await?;
1837 let t = client.task_info(self.id()).await?;
1838 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1839 Ok(())
1840 }
1841
1842 pub async fn data_list(&self, client: &client::Client) -> Result<TaskDataList, Error> {
1858 let req = client::TaskDataListRequest {
1859 task_id: self.id().value(),
1860 };
1861 match client.rpc("task.data.list".to_owned(), Some(&req)).await {
1862 Ok(r) => Ok(r),
1863 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1864 "task.data.list",
1865 code,
1866 msg,
1867 Some(self.id()),
1868 )),
1869 Err(e) => Err(e),
1870 }
1871 }
1872
1873 pub async fn upload_data(
1894 &self,
1895 client: &client::Client,
1896 path: &std::path::Path,
1897 folder: Option<&str>,
1898 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1899 ) -> Result<(), Error> {
1900 use futures::StreamExt;
1901 use std::sync::{
1902 Arc,
1903 atomic::{AtomicUsize, Ordering},
1904 };
1905 use tokio_util::io::ReaderStream;
1906
1907 let file_name = path
1908 .file_name()
1909 .and_then(|s| s.to_str())
1910 .ok_or_else(|| Error::InvalidParameters("path must have a UTF-8 filename".into()))?
1911 .to_owned();
1912
1913 let file = tokio::fs::File::open(path).await?;
1914 let total = file.metadata().await?.len() as usize;
1915 let sent = Arc::new(AtomicUsize::new(0));
1916
1917 let reader_stream = ReaderStream::new(file);
1918 let sent_clone = sent.clone();
1919 let progress_clone = progress.clone();
1920 let progress_stream = reader_stream.inspect(move |chunk_result| {
1921 if let Ok(chunk) = chunk_result {
1922 let current = sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1923 if let Some(tx) = &progress_clone {
1929 let _ = tx.try_send(Progress {
1930 current,
1931 total,
1932 status: None,
1933 });
1934 }
1935 }
1936 });
1937
1938 let body = reqwest::Body::wrap_stream(progress_stream);
1939 let file_part = Part::stream_with_length(body, total as u64).file_name(file_name);
1940
1941 let mut form = Form::new().text("task_id", self.id().value().to_string());
1942 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
1943 form = form.text("folder", folder.to_owned());
1944 }
1945 form = form.part("file", file_part);
1946
1947 let result = match client.post_multipart("task.data.upload", form).await {
1948 Ok(_) => Ok(()),
1949 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1950 "task.data.upload",
1951 code,
1952 msg,
1953 Some(self.id()),
1954 )),
1955 Err(e) => Err(e),
1956 };
1957
1958 if result.is_ok()
1962 && let Some(tx) = progress
1963 {
1964 let _ = tx
1965 .send(Progress {
1966 current: total,
1967 total,
1968 status: None,
1969 })
1970 .await;
1971 }
1972 result
1973 }
1974
1975 pub async fn download_data(
2004 &self,
2005 client: &client::Client,
2006 file: &str,
2007 folder: Option<&str>,
2008 output_path: &std::path::Path,
2009 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
2010 ) -> Result<(), Error> {
2011 let folder = folder.unwrap_or("").to_owned();
2012 let req = client::TaskDataDownloadRequest {
2013 task_id: self.id().value(),
2014 folder,
2015 file: file.to_owned(),
2016 };
2017 match client
2018 .rpc_download("task.data.download", &req, output_path, progress)
2019 .await
2020 {
2021 Ok(()) => Ok(()),
2022 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2023 "task.data.download",
2024 code,
2025 msg,
2026 Some(self.id()),
2027 )),
2028 Err(e) => Err(e),
2029 }
2030 }
2031
2032 pub async fn add_chart(
2060 &self,
2061 client: &client::Client,
2062 group: &str,
2063 name: &str,
2064 data: Parameter,
2065 params: Option<Parameter>,
2066 ) -> Result<(), Error> {
2067 client::validate_chart_args(group, name)?;
2068 let req = client::TaskChartAddRequest {
2069 task_id: self.id().value(),
2070 group_name: group.to_owned(),
2071 chart_name: name.to_owned(),
2072 params,
2073 data,
2074 };
2075 let _resp: serde_json::Value =
2076 match client.rpc("task.chart.add".to_owned(), Some(&req)).await {
2077 Ok(r) => r,
2078 Err(Error::RpcError(code, msg)) => {
2079 return Err(client::map_rpc_error(
2080 "task.chart.add",
2081 code,
2082 msg,
2083 Some(self.id()),
2084 ));
2085 }
2086 Err(e) => return Err(e),
2087 };
2088 Ok(())
2089 }
2090
2091 pub async fn list_charts(
2108 &self,
2109 client: &client::Client,
2110 group: Option<&str>,
2111 ) -> Result<TaskDataList, Error> {
2112 let req = client::TaskChartListRequest {
2113 task_id: self.id().value(),
2114 group_name: group.unwrap_or("").to_owned(),
2115 };
2116 match client.rpc("task.chart.list".to_owned(), Some(&req)).await {
2117 Ok(r) => Ok(r),
2118 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2119 "task.chart.list",
2120 code,
2121 msg,
2122 Some(self.id()),
2123 )),
2124 Err(e) => Err(e),
2125 }
2126 }
2127
2128 pub async fn get_chart(
2147 &self,
2148 client: &client::Client,
2149 group: &str,
2150 name: &str,
2151 ) -> Result<Parameter, Error> {
2152 client::validate_chart_args(group, name)?;
2153 let req = client::TaskChartGetRequest {
2154 task_id: self.id().value(),
2155 group_name: group.to_owned(),
2156 chart_name: name.to_owned(),
2157 };
2158 match client.rpc("task.chart.get".to_owned(), Some(&req)).await {
2159 Ok(r) => Ok(r),
2160 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2161 "task.chart.get",
2162 code,
2163 msg,
2164 Some(self.id()),
2165 )),
2166 Err(e) => Err(e),
2167 }
2168 }
2169
2170 pub fn created(&self) -> &DateTime<Utc> {
2171 &self.created
2172 }
2173
2174 pub fn completed(&self) -> &DateTime<Utc> {
2175 &self.completed
2176 }
2177}
2178
2179#[derive(Deserialize, Debug, Default, Clone)]
2180pub struct TaskProgress {
2181 stages: Option<HashMap<String, Stage>>,
2182}
2183
2184#[derive(Serialize, Debug, Clone)]
2185pub struct TaskStatus {
2186 #[serde(rename = "docker_task_id")]
2187 pub task_id: TaskID,
2188 pub status: String,
2189}
2190
2191#[derive(Serialize, Deserialize, Debug, Clone)]
2192pub struct Stage {
2193 #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
2194 task_id: Option<TaskID>,
2195 stage: String,
2196 #[serde(skip_serializing_if = "Option::is_none")]
2197 status: Option<String>,
2198 #[serde(skip_serializing_if = "Option::is_none")]
2199 description: Option<String>,
2200 #[serde(skip_serializing_if = "Option::is_none")]
2201 message: Option<String>,
2202 percentage: u8,
2203}
2204
2205impl Stage {
2206 pub fn new(
2207 task_id: Option<TaskID>,
2208 stage: String,
2209 status: Option<String>,
2210 message: Option<String>,
2211 percentage: u8,
2212 ) -> Self {
2213 Stage {
2214 task_id,
2215 stage,
2216 status,
2217 description: None,
2218 message,
2219 percentage,
2220 }
2221 }
2222
2223 pub fn task_id(&self) -> &Option<TaskID> {
2224 &self.task_id
2225 }
2226
2227 pub fn stage(&self) -> &str {
2228 &self.stage
2229 }
2230
2231 pub fn status(&self) -> &Option<String> {
2232 &self.status
2233 }
2234
2235 pub fn description(&self) -> &Option<String> {
2236 &self.description
2237 }
2238
2239 pub fn message(&self) -> &Option<String> {
2240 &self.message
2241 }
2242
2243 pub fn percentage(&self) -> u8 {
2244 self.percentage
2245 }
2246}
2247
2248#[derive(Serialize, Debug)]
2249pub struct TaskStages {
2250 #[serde(rename = "docker_task_id")]
2251 pub task_id: TaskID,
2252 #[serde(skip_serializing_if = "Vec::is_empty")]
2253 pub stages: Vec<HashMap<String, String>>,
2254}
2255
2256#[derive(Deserialize, Debug)]
2257pub struct Artifact {
2258 name: String,
2259 #[serde(rename = "modelType")]
2260 model_type: String,
2261}
2262
2263impl Artifact {
2264 pub fn name(&self) -> &str {
2265 &self.name
2266 }
2267
2268 pub fn model_type(&self) -> &str {
2269 &self.model_type
2270 }
2271}
2272
2273#[derive(Deserialize, Serialize, Clone, Debug)]
2281pub struct VersionTag {
2282 id: u64,
2283 dataset_id: DatasetID,
2284 name: String,
2285 serial: u64,
2286 #[serde(default)]
2287 description: String,
2288 created_by: String,
2289 created_at: DateTime<Utc>,
2290 #[serde(default)]
2291 image_count: u64,
2292 #[serde(default)]
2293 annotation_counts: HashMap<String, u64>,
2294 #[serde(default)]
2295 sensor_counts: HashMap<String, u64>,
2296 #[serde(default)]
2297 label_count: u64,
2298 #[serde(default)]
2299 annotation_set_count: u64,
2300 #[serde(default)]
2301 snapshot_id: Option<u64>,
2302 #[serde(default)]
2303 is_current: bool,
2304}
2305
2306impl VersionTag {
2307 pub fn id(&self) -> u64 {
2309 self.id
2310 }
2311
2312 pub fn dataset_id(&self) -> DatasetID {
2314 self.dataset_id
2315 }
2316
2317 pub fn name(&self) -> &str {
2319 &self.name
2320 }
2321
2322 pub fn serial(&self) -> u64 {
2324 self.serial
2325 }
2326
2327 pub fn description(&self) -> &str {
2329 &self.description
2330 }
2331
2332 pub fn created_by(&self) -> &str {
2334 &self.created_by
2335 }
2336
2337 pub fn created_at(&self) -> DateTime<Utc> {
2339 self.created_at
2340 }
2341
2342 pub fn image_count(&self) -> u64 {
2344 self.image_count
2345 }
2346
2347 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2349 &self.annotation_counts
2350 }
2351
2352 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2354 &self.sensor_counts
2355 }
2356
2357 pub fn label_count(&self) -> u64 {
2359 self.label_count
2360 }
2361
2362 pub fn annotation_set_count(&self) -> u64 {
2364 self.annotation_set_count
2365 }
2366
2367 pub fn snapshot_id(&self) -> Option<u64> {
2369 self.snapshot_id
2370 }
2371
2372 pub fn is_current(&self) -> bool {
2375 self.is_current
2376 }
2377}
2378
2379impl Display for VersionTag {
2380 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2381 write!(f, "{} (serial {})", self.name, self.serial)
2382 }
2383}
2384
2385#[derive(Deserialize, Serialize, Clone, Debug)]
2387pub struct ChangelogEntry {
2388 id: u64,
2389 dataset_id: DatasetID,
2390 serial: u64,
2391 entity_type: String,
2392 operation: String,
2393 #[serde(default)]
2394 entity_id: Option<u64>,
2395 #[serde(default)]
2396 change_data: serde_json::Value,
2397 username: String,
2398 organization_id: u64,
2399 created_at: DateTime<Utc>,
2400 #[serde(default)]
2401 message: String,
2402 #[serde(default, deserialize_with = "deserialize_null_as_default")]
2403 s3_version_ids: Vec<serde_json::Value>,
2404}
2405
2406impl ChangelogEntry {
2407 pub fn id(&self) -> u64 {
2408 self.id
2409 }
2410
2411 pub fn dataset_id(&self) -> DatasetID {
2412 self.dataset_id
2413 }
2414
2415 pub fn serial(&self) -> u64 {
2417 self.serial
2418 }
2419
2420 pub fn entity_type(&self) -> &str {
2422 &self.entity_type
2423 }
2424
2425 pub fn operation(&self) -> &str {
2427 &self.operation
2428 }
2429
2430 pub fn entity_id(&self) -> Option<u64> {
2431 self.entity_id
2432 }
2433
2434 pub fn change_data(&self) -> &serde_json::Value {
2436 &self.change_data
2437 }
2438
2439 pub fn username(&self) -> &str {
2440 &self.username
2441 }
2442
2443 pub fn organization_id(&self) -> u64 {
2444 self.organization_id
2445 }
2446
2447 pub fn created_at(&self) -> DateTime<Utc> {
2448 self.created_at
2449 }
2450
2451 pub fn message(&self) -> &str {
2452 &self.message
2453 }
2454
2455 pub fn s3_version_ids(&self) -> &[serde_json::Value] {
2456 &self.s3_version_ids
2457 }
2458}
2459
2460#[derive(Deserialize, Debug, Clone)]
2462pub struct ChangelogResponse {
2463 pub entries: Vec<ChangelogEntry>,
2464 pub count: u64,
2465 #[serde(default)]
2466 pub continue_token: String,
2467 #[serde(default)]
2468 pub from_serial: Option<u64>,
2469 #[serde(default)]
2470 pub to_serial: Option<u64>,
2471}
2472
2473#[derive(Deserialize, Serialize, Clone, Debug)]
2475pub struct DatasetSummary {
2476 dataset_id: DatasetID,
2477 current_serial: u64,
2478 #[serde(default)]
2479 image_count: u64,
2480 #[serde(default)]
2481 annotation_counts: HashMap<String, u64>,
2482 #[serde(default)]
2483 sensor_counts: HashMap<String, u64>,
2484 #[serde(default)]
2485 label_count: u64,
2486 #[serde(default)]
2487 annotation_set_count: u64,
2488 last_updated: DateTime<Utc>,
2489}
2490
2491impl DatasetSummary {
2492 pub fn dataset_id(&self) -> DatasetID {
2493 self.dataset_id
2494 }
2495
2496 pub fn current_serial(&self) -> u64 {
2497 self.current_serial
2498 }
2499
2500 pub fn image_count(&self) -> u64 {
2501 self.image_count
2502 }
2503
2504 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2505 &self.annotation_counts
2506 }
2507
2508 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2509 &self.sensor_counts
2510 }
2511
2512 pub fn label_count(&self) -> u64 {
2513 self.label_count
2514 }
2515
2516 pub fn annotation_set_count(&self) -> u64 {
2517 self.annotation_set_count
2518 }
2519
2520 pub fn last_updated(&self) -> DateTime<Utc> {
2521 self.last_updated
2522 }
2523}
2524
2525#[derive(Deserialize, Debug, Clone)]
2527pub struct VersionCurrentResponse {
2528 pub dataset_id: DatasetID,
2529 pub current_serial: u64,
2530 #[serde(default)]
2531 pub latest_tag: Option<VersionTag>,
2532 #[serde(default)]
2533 pub tags: Vec<VersionTag>,
2534 #[serde(default)]
2535 pub summary: Option<DatasetSummary>,
2536}
2537
2538#[derive(Deserialize, Debug, Clone)]
2540pub struct RestoredFrom {
2541 pub tag: String,
2542 pub serial: u64,
2543}
2544
2545#[derive(Deserialize, Debug, Clone)]
2547pub struct RestoredCounts {
2548 pub images: u64,
2549 pub labels: u64,
2550 pub annotation_sets: u64,
2551}
2552
2553#[derive(Deserialize, Debug, Clone)]
2555pub struct RestoreResult {
2556 pub success: bool,
2557 pub new_serial: u64,
2558 pub restored_from: RestoredFrom,
2559 pub restored_counts: RestoredCounts,
2560 pub message: String,
2561}
2562
2563#[derive(Serialize)]
2566pub(crate) struct VersionTagCreateParams {
2567 pub dataset_id: DatasetID,
2568 pub name: String,
2569 #[serde(skip_serializing_if = "Option::is_none")]
2570 pub description: Option<String>,
2571}
2572
2573#[derive(Serialize)]
2574pub(crate) struct VersionTagNameParams {
2575 pub dataset_id: DatasetID,
2576 pub name: String,
2577}
2578
2579#[derive(Serialize)]
2580pub(crate) struct VersionChangelogParams {
2581 pub dataset_id: DatasetID,
2582 #[serde(skip_serializing_if = "Option::is_none")]
2583 pub from_version: Option<String>,
2584 #[serde(skip_serializing_if = "Option::is_none")]
2585 pub to_version: Option<String>,
2586 #[serde(skip_serializing_if = "Option::is_none")]
2587 pub entity_types: Option<Vec<String>>,
2588 #[serde(skip_serializing_if = "Option::is_none")]
2589 pub limit: Option<u64>,
2590 #[serde(skip_serializing_if = "Option::is_none")]
2591 pub continue_token: Option<String>,
2592}
2593
2594#[derive(Deserialize, Debug)]
2596pub(crate) struct ChangelogCountResult {
2597 pub count: u64,
2598}
2599
2600#[derive(Serialize, Deserialize, Debug, Clone)]
2607pub struct TrainerSchemaInfo {
2608 pub name: String,
2610 #[serde(default)]
2612 pub label: String,
2613 #[serde(default)]
2615 pub schema_type: String,
2616}
2617
2618#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
2624#[serde(rename_all = "lowercase")]
2625pub enum SchemaFieldType {
2626 Group,
2628 Slider,
2630 Select,
2632 Bool,
2634 Int,
2636 Float,
2638 Text,
2640 Date,
2642 Project,
2644 Dataset,
2646 Trainer,
2648 Upload,
2650 Info,
2653 #[serde(other)]
2655 Unknown,
2656}
2657
2658fn lenient_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
2662where
2663 D: Deserializer<'de>,
2664{
2665 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
2666 Ok(value.map(|v| match v {
2667 serde_json::Value::String(s) => s,
2668 other => other.to_string(),
2669 }))
2670}
2671
2672#[derive(Serialize, Deserialize, Debug, Clone)]
2674pub struct SchemaOption {
2675 #[serde(default)]
2677 pub name: Option<Parameter>,
2678 #[serde(default, deserialize_with = "lenient_string")]
2681 pub label: Option<String>,
2682 #[serde(default)]
2684 pub children: Vec<SchemaField>,
2685}
2686
2687#[derive(Serialize, Deserialize, Debug, Clone)]
2699pub struct SchemaField {
2700 #[serde(default, deserialize_with = "lenient_string")]
2702 pub name: Option<String>,
2703 #[serde(default, deserialize_with = "lenient_string")]
2705 pub label: Option<String>,
2706 #[serde(default, deserialize_with = "lenient_string")]
2708 pub description: Option<String>,
2709 #[serde(default)]
2711 pub required: bool,
2712 #[serde(default)]
2714 pub default: Option<Parameter>,
2715 #[serde(rename = "type", default)]
2717 pub field_type: Option<SchemaFieldType>,
2718 #[serde(default)]
2720 pub min: Option<f64>,
2721 #[serde(default)]
2723 pub max: Option<f64>,
2724 #[serde(default)]
2726 pub step: Option<f64>,
2727 #[serde(default)]
2729 pub options: Vec<SchemaOption>,
2730 #[serde(default)]
2733 pub children: Vec<SchemaField>,
2734 #[serde(default)]
2736 pub is_dropdown: bool,
2737 #[serde(default)]
2739 pub multi_select: bool,
2740 #[serde(default)]
2742 pub is_multi_line: bool,
2743 #[serde(default)]
2745 pub hidden: bool,
2746 #[serde(default)]
2748 pub numeric_only: bool,
2749 #[serde(default)]
2751 pub enable_tags_selection: bool,
2752 #[serde(default)]
2754 pub enable_annotation_set_selection: bool,
2755 #[serde(default)]
2757 pub values: Option<Vec<Parameter>>,
2758}
2759
2760#[derive(Serialize, Deserialize, Debug, Clone)]
2763pub struct ValidatorSchema {
2764 #[serde(rename = "type", default)]
2766 pub schema_type: String,
2767 #[serde(default)]
2769 pub name: String,
2770 #[serde(default)]
2772 pub schema: Vec<SchemaField>,
2773}
2774
2775#[cfg(test)]
2776mod tests {
2777 use super::*;
2778
2779 #[test]
2781 fn test_organization_id_from_u64() {
2782 let id = OrganizationID::from(12345);
2783 assert_eq!(id.value(), 12345);
2784 }
2785
2786 #[test]
2787 fn test_organization_id_display() {
2788 let id = OrganizationID::from(0xabc123);
2789 assert_eq!(format!("{}", id), "org-abc123");
2790 }
2791
2792 #[test]
2793 fn test_organization_id_try_from_str_valid() {
2794 let id = OrganizationID::try_from("org-abc123").unwrap();
2795 assert_eq!(id.value(), 0xabc123);
2796 }
2797
2798 #[test]
2799 fn test_organization_id_try_from_str_invalid_prefix() {
2800 let result = OrganizationID::try_from("invalid-abc123");
2801 assert!(result.is_err());
2802 match result {
2803 Err(Error::InvalidParameters(msg)) => {
2804 assert!(msg.contains("must start with 'org-'"));
2805 }
2806 _ => panic!("Expected InvalidParameters error"),
2807 }
2808 }
2809
2810 #[test]
2811 fn test_organization_id_try_from_str_invalid_hex() {
2812 let result = OrganizationID::try_from("org-xyz");
2813 assert!(result.is_err());
2814 }
2815
2816 #[test]
2817 fn test_organization_id_try_from_str_empty() {
2818 let result = OrganizationID::try_from("org-");
2819 assert!(result.is_err());
2820 }
2821
2822 #[test]
2823 fn test_organization_id_into_u64() {
2824 let id = OrganizationID::from(54321);
2825 let value: u64 = id.into();
2826 assert_eq!(value, 54321);
2827 }
2828
2829 #[test]
2831 fn test_usage_summary_deserialize_and_accessors() {
2832 let usage: UsageSummary = serde_json::from_str(
2833 r#"{"credits": 12.5, "funds": 49092.92, "total_funds_and_credits": 49105.42}"#,
2834 )
2835 .unwrap();
2836 assert_eq!(usage.credits(), 12.5);
2837 assert_eq!(usage.funds(), 49092.92);
2838 assert_eq!(usage.total(), 49105.42);
2839 }
2840
2841 #[test]
2842 fn test_usage_summary_defaults_for_missing_fields() {
2843 let usage: UsageSummary = serde_json::from_str("{}").unwrap();
2847 assert_eq!(usage.credits(), 0.0);
2848 assert_eq!(usage.funds(), 0.0);
2849 assert_eq!(usage.total(), 0.0);
2850 }
2851
2852 #[test]
2854 fn test_project_id_from_u64() {
2855 let id = ProjectID::from(78910);
2856 assert_eq!(id.value(), 78910);
2857 }
2858
2859 #[test]
2860 fn test_project_id_display() {
2861 let id = ProjectID::from(0xdef456);
2862 assert_eq!(format!("{}", id), "p-def456");
2863 }
2864
2865 #[test]
2866 fn test_project_id_from_str_valid() {
2867 let id = ProjectID::from_str("p-def456").unwrap();
2868 assert_eq!(id.value(), 0xdef456);
2869 }
2870
2871 #[test]
2872 fn test_project_id_try_from_str_valid() {
2873 let id = ProjectID::try_from("p-123abc").unwrap();
2874 assert_eq!(id.value(), 0x123abc);
2875 }
2876
2877 #[test]
2878 fn test_project_id_try_from_string_valid() {
2879 let id = ProjectID::try_from("p-456def".to_string()).unwrap();
2880 assert_eq!(id.value(), 0x456def);
2881 }
2882
2883 #[test]
2884 fn test_project_id_from_str_invalid_prefix() {
2885 let result = ProjectID::from_str("proj-123");
2886 assert!(result.is_err());
2887 match result {
2888 Err(Error::InvalidParameters(msg)) => {
2889 assert!(msg.contains("must start with 'p-'"));
2890 }
2891 _ => panic!("Expected InvalidParameters error"),
2892 }
2893 }
2894
2895 #[test]
2896 fn test_project_id_from_str_invalid_hex() {
2897 let result = ProjectID::from_str("p-notahex");
2898 assert!(result.is_err());
2899 }
2900
2901 #[test]
2902 fn test_project_id_into_u64() {
2903 let id = ProjectID::from(99999);
2904 let value: u64 = id.into();
2905 assert_eq!(value, 99999);
2906 }
2907
2908 #[test]
2910 fn test_experiment_id_from_u64() {
2911 let id = ExperimentID::from(1193046);
2912 assert_eq!(id.value(), 1193046);
2913 }
2914
2915 #[test]
2916 fn test_experiment_id_display() {
2917 let id = ExperimentID::from(0x123abc);
2918 assert_eq!(format!("{}", id), "exp-123abc");
2919 }
2920
2921 #[test]
2922 fn test_experiment_id_from_str_valid() {
2923 let id = ExperimentID::from_str("exp-456def").unwrap();
2924 assert_eq!(id.value(), 0x456def);
2925 }
2926
2927 #[test]
2928 fn test_experiment_id_try_from_str_valid() {
2929 let id = ExperimentID::try_from("exp-789abc").unwrap();
2930 assert_eq!(id.value(), 0x789abc);
2931 }
2932
2933 #[test]
2934 fn test_experiment_id_try_from_string_valid() {
2935 let id = ExperimentID::try_from("exp-fedcba".to_string()).unwrap();
2936 assert_eq!(id.value(), 0xfedcba);
2937 }
2938
2939 #[test]
2940 fn test_experiment_id_from_str_invalid_prefix() {
2941 let result = ExperimentID::from_str("experiment-123");
2942 assert!(result.is_err());
2943 match result {
2944 Err(Error::InvalidParameters(msg)) => {
2945 assert!(msg.contains("must start with 'exp-'"));
2946 }
2947 _ => panic!("Expected InvalidParameters error"),
2948 }
2949 }
2950
2951 #[test]
2952 fn test_experiment_id_from_str_invalid_hex() {
2953 let result = ExperimentID::from_str("exp-zzz");
2954 assert!(result.is_err());
2955 }
2956
2957 #[test]
2958 fn test_experiment_id_into_u64() {
2959 let id = ExperimentID::from(777777);
2960 let value: u64 = id.into();
2961 assert_eq!(value, 777777);
2962 }
2963
2964 #[test]
2966 fn test_training_session_id_from_u64() {
2967 let id = TrainingSessionID::from(7901234);
2968 assert_eq!(id.value(), 7901234);
2969 }
2970
2971 #[test]
2972 fn test_training_session_id_display() {
2973 let id = TrainingSessionID::from(0xabc123);
2974 assert_eq!(format!("{}", id), "t-abc123");
2975 }
2976
2977 #[test]
2978 fn test_training_session_id_from_str_valid() {
2979 let id = TrainingSessionID::from_str("t-abc123").unwrap();
2980 assert_eq!(id.value(), 0xabc123);
2981 }
2982
2983 #[test]
2984 fn test_training_session_id_try_from_str_valid() {
2985 let id = TrainingSessionID::try_from("t-deadbeef").unwrap();
2986 assert_eq!(id.value(), 0xdeadbeef);
2987 }
2988
2989 #[test]
2990 fn test_training_session_id_try_from_string_valid() {
2991 let id = TrainingSessionID::try_from("t-cafebabe".to_string()).unwrap();
2992 assert_eq!(id.value(), 0xcafebabe);
2993 }
2994
2995 #[test]
2996 fn test_training_session_id_from_str_invalid_prefix() {
2997 let result = TrainingSessionID::from_str("training-123");
2998 assert!(result.is_err());
2999 match result {
3000 Err(Error::InvalidParameters(msg)) => {
3001 assert!(msg.contains("must start with 't-'"));
3002 }
3003 _ => panic!("Expected InvalidParameters error"),
3004 }
3005 }
3006
3007 #[test]
3008 fn test_training_session_id_from_str_invalid_hex() {
3009 let result = TrainingSessionID::from_str("t-qqq");
3010 assert!(result.is_err());
3011 }
3012
3013 #[test]
3014 fn test_training_session_id_into_u64() {
3015 let id = TrainingSessionID::from(123456);
3016 let value: u64 = id.into();
3017 assert_eq!(value, 123456);
3018 }
3019
3020 #[test]
3022 fn test_validation_session_id_from_u64() {
3023 let id = ValidationSessionID::from(3456789);
3024 assert_eq!(id.value(), 3456789);
3025 }
3026
3027 #[test]
3028 fn test_validation_session_id_display() {
3029 let id = ValidationSessionID::from(0x34c985);
3030 assert_eq!(format!("{}", id), "v-34c985");
3031 }
3032
3033 #[test]
3034 fn test_validation_session_id_try_from_str_valid() {
3035 let id = ValidationSessionID::try_from("v-deadbeef").unwrap();
3036 assert_eq!(id.value(), 0xdeadbeef);
3037 }
3038
3039 #[test]
3040 fn test_validation_session_id_try_from_string_valid() {
3041 let id = ValidationSessionID::try_from("v-12345678".to_string()).unwrap();
3042 assert_eq!(id.value(), 0x12345678);
3043 }
3044
3045 #[test]
3046 fn test_validation_session_id_try_from_str_invalid_prefix() {
3047 let result = ValidationSessionID::try_from("validation-123");
3048 assert!(result.is_err());
3049 match result {
3050 Err(Error::InvalidParameters(msg)) => {
3051 assert!(msg.contains("must start with 'v-'"));
3052 }
3053 _ => panic!("Expected InvalidParameters error"),
3054 }
3055 }
3056
3057 #[test]
3058 fn test_validation_session_id_try_from_str_invalid_hex() {
3059 let result = ValidationSessionID::try_from("v-xyz");
3060 assert!(result.is_err());
3061 }
3062
3063 #[test]
3064 fn test_validation_session_id_into_u64() {
3065 let id = ValidationSessionID::from(987654);
3066 let value: u64 = id.into();
3067 assert_eq!(value, 987654);
3068 }
3069
3070 #[test]
3072 fn test_snapshot_id_from_u64() {
3073 let id = SnapshotID::from(111222);
3074 assert_eq!(id.value(), 111222);
3075 }
3076
3077 #[test]
3078 fn test_snapshot_id_display() {
3079 let id = SnapshotID::from(0xaabbcc);
3080 assert_eq!(format!("{}", id), "ss-aabbcc");
3081 }
3082
3083 #[test]
3084 fn test_snapshot_id_try_from_str_valid() {
3085 let id = SnapshotID::try_from("ss-aabbcc").unwrap();
3086 assert_eq!(id.value(), 0xaabbcc);
3087 }
3088
3089 #[test]
3090 fn test_snapshot_id_try_from_str_invalid_prefix() {
3091 let result = SnapshotID::try_from("snapshot-123");
3092 assert!(result.is_err());
3093 match result {
3094 Err(Error::InvalidParameters(msg)) => {
3095 assert!(msg.contains("must start with 'ss-'"));
3096 }
3097 _ => panic!("Expected InvalidParameters error"),
3098 }
3099 }
3100
3101 #[test]
3102 fn test_snapshot_id_try_from_str_invalid_hex() {
3103 let result = SnapshotID::try_from("ss-ggg");
3104 assert!(result.is_err());
3105 }
3106
3107 #[test]
3108 fn test_snapshot_id_into_u64() {
3109 let id = SnapshotID::from(333444);
3110 let value: u64 = id.into();
3111 assert_eq!(value, 333444);
3112 }
3113
3114 #[test]
3116 fn test_task_id_from_u64() {
3117 let id = TaskID::from(555666);
3118 assert_eq!(id.value(), 555666);
3119 }
3120
3121 #[test]
3122 fn test_task_id_display() {
3123 let id = TaskID::from(0x123456);
3124 assert_eq!(format!("{}", id), "task-123456");
3125 }
3126
3127 #[test]
3128 fn test_task_id_from_str_valid() {
3129 let id = TaskID::from_str("task-123456").unwrap();
3130 assert_eq!(id.value(), 0x123456);
3131 }
3132
3133 #[test]
3134 fn test_task_id_try_from_str_valid() {
3135 let id = TaskID::try_from("task-abcdef").unwrap();
3136 assert_eq!(id.value(), 0xabcdef);
3137 }
3138
3139 #[test]
3140 fn test_task_id_try_from_string_valid() {
3141 let id = TaskID::try_from("task-fedcba".to_string()).unwrap();
3142 assert_eq!(id.value(), 0xfedcba);
3143 }
3144
3145 #[test]
3146 fn test_task_id_from_str_invalid_prefix() {
3147 let result = TaskID::from_str("t-123");
3148 assert!(result.is_err());
3149 match result {
3150 Err(Error::InvalidParameters(msg)) => {
3151 assert!(msg.contains("must start with 'task-'"));
3152 }
3153 _ => panic!("Expected InvalidParameters error"),
3154 }
3155 }
3156
3157 #[test]
3158 fn test_task_id_from_str_invalid_hex() {
3159 let result = TaskID::from_str("task-zzz");
3160 assert!(result.is_err());
3161 }
3162
3163 #[test]
3164 fn test_task_id_into_u64() {
3165 let id = TaskID::from(777888);
3166 let value: u64 = id.into();
3167 assert_eq!(value, 777888);
3168 }
3169
3170 #[test]
3172 fn test_dataset_id_from_u64() {
3173 let id = DatasetID::from(1193046);
3174 assert_eq!(id.value(), 1193046);
3175 }
3176
3177 #[test]
3178 fn test_dataset_id_display() {
3179 let id = DatasetID::from(0x123abc);
3180 assert_eq!(format!("{}", id), "ds-123abc");
3181 }
3182
3183 #[test]
3184 fn test_dataset_id_from_str_valid() {
3185 let id = DatasetID::from_str("ds-456def").unwrap();
3186 assert_eq!(id.value(), 0x456def);
3187 }
3188
3189 #[test]
3190 fn test_dataset_id_try_from_str_valid() {
3191 let id = DatasetID::try_from("ds-789abc").unwrap();
3192 assert_eq!(id.value(), 0x789abc);
3193 }
3194
3195 #[test]
3196 fn test_dataset_id_try_from_string_valid() {
3197 let id = DatasetID::try_from("ds-fedcba".to_string()).unwrap();
3198 assert_eq!(id.value(), 0xfedcba);
3199 }
3200
3201 #[test]
3202 fn test_dataset_id_from_str_invalid_prefix() {
3203 let result = DatasetID::from_str("dataset-123");
3204 assert!(result.is_err());
3205 match result {
3206 Err(Error::InvalidParameters(msg)) => {
3207 assert!(msg.contains("must start with 'ds-'"));
3208 }
3209 _ => panic!("Expected InvalidParameters error"),
3210 }
3211 }
3212
3213 #[test]
3214 fn test_dataset_id_from_str_invalid_hex() {
3215 let result = DatasetID::from_str("ds-zzz");
3216 assert!(result.is_err());
3217 }
3218
3219 #[test]
3220 fn test_dataset_id_into_u64() {
3221 let id = DatasetID::from(111111);
3222 let value: u64 = id.into();
3223 assert_eq!(value, 111111);
3224 }
3225
3226 #[test]
3227 fn dataset_id_default_is_zero() {
3228 assert_eq!(DatasetID::default().value(), 0);
3229 }
3230
3231 #[test]
3232 fn dataset_params_default_is_all_zero_and_empty() {
3233 let params = DatasetParams::default();
3234 assert_eq!(params.dataset_id().value(), 0);
3235 assert_eq!(params.annotation_set_id().value(), 0);
3236 assert_eq!(params.train_group(), "");
3237 assert_eq!(params.val_group(), "");
3238 }
3239
3240 #[test]
3242 fn test_annotation_set_id_from_u64() {
3243 let id = AnnotationSetID::from(222333);
3244 assert_eq!(id.value(), 222333);
3245 }
3246
3247 #[test]
3248 fn test_annotation_set_id_display() {
3249 let id = AnnotationSetID::from(0xabcdef);
3250 assert_eq!(format!("{}", id), "as-abcdef");
3251 }
3252
3253 #[test]
3254 fn test_annotation_set_id_from_str_valid() {
3255 let id = AnnotationSetID::from_str("as-abcdef").unwrap();
3256 assert_eq!(id.value(), 0xabcdef);
3257 }
3258
3259 #[test]
3260 fn test_annotation_set_id_try_from_str_valid() {
3261 let id = AnnotationSetID::try_from("as-123456").unwrap();
3262 assert_eq!(id.value(), 0x123456);
3263 }
3264
3265 #[test]
3266 fn test_annotation_set_id_try_from_string_valid() {
3267 let id = AnnotationSetID::try_from("as-fedcba".to_string()).unwrap();
3268 assert_eq!(id.value(), 0xfedcba);
3269 }
3270
3271 #[test]
3272 fn test_annotation_set_id_from_str_invalid_prefix() {
3273 let result = AnnotationSetID::from_str("annotation-123");
3274 assert!(result.is_err());
3275 match result {
3276 Err(Error::InvalidParameters(msg)) => {
3277 assert!(msg.contains("must start with 'as-'"));
3278 }
3279 _ => panic!("Expected InvalidParameters error"),
3280 }
3281 }
3282
3283 #[test]
3284 fn test_annotation_set_id_from_str_invalid_hex() {
3285 let result = AnnotationSetID::from_str("as-zzz");
3286 assert!(result.is_err());
3287 }
3288
3289 #[test]
3290 fn test_annotation_set_id_into_u64() {
3291 let id = AnnotationSetID::from(444555);
3292 let value: u64 = id.into();
3293 assert_eq!(value, 444555);
3294 }
3295
3296 #[test]
3298 fn test_sample_id_from_u64() {
3299 let id = SampleID::from(666777);
3300 assert_eq!(id.value(), 666777);
3301 }
3302
3303 #[test]
3304 fn test_sample_id_display() {
3305 let id = SampleID::from(0x987654);
3306 assert_eq!(format!("{}", id), "s-987654");
3307 }
3308
3309 #[test]
3310 fn test_sample_id_try_from_str_valid() {
3311 let id = SampleID::try_from("s-987654").unwrap();
3312 assert_eq!(id.value(), 0x987654);
3313 }
3314
3315 #[test]
3316 fn test_sample_id_try_from_str_invalid_prefix() {
3317 let result = SampleID::try_from("sample-123");
3318 assert!(result.is_err());
3319 match result {
3320 Err(Error::InvalidParameters(msg)) => {
3321 assert!(msg.contains("must start with 's-'"));
3322 }
3323 _ => panic!("Expected InvalidParameters error"),
3324 }
3325 }
3326
3327 #[test]
3328 fn test_sample_id_try_from_str_invalid_hex() {
3329 let result = SampleID::try_from("s-zzz");
3330 assert!(result.is_err());
3331 }
3332
3333 #[test]
3334 fn test_sample_id_into_u64() {
3335 let id = SampleID::from(888999);
3336 let value: u64 = id.into();
3337 assert_eq!(value, 888999);
3338 }
3339
3340 #[test]
3342 fn test_app_id_from_u64() {
3343 let id = AppId::from(123123);
3344 assert_eq!(id.value(), 123123);
3345 }
3346
3347 #[test]
3348 fn test_app_id_display() {
3349 let id = AppId::from(0x456789);
3350 assert_eq!(format!("{}", id), "app-456789");
3351 }
3352
3353 #[test]
3354 fn test_app_id_try_from_str_valid() {
3355 let id = AppId::try_from("app-456789").unwrap();
3356 assert_eq!(id.value(), 0x456789);
3357 }
3358
3359 #[test]
3360 fn test_app_id_try_from_str_invalid_prefix() {
3361 let result = AppId::try_from("application-123");
3362 assert!(result.is_err());
3363 match result {
3364 Err(Error::InvalidParameters(msg)) => {
3365 assert!(msg.contains("must start with 'app-'"));
3366 }
3367 _ => panic!("Expected InvalidParameters error"),
3368 }
3369 }
3370
3371 #[test]
3372 fn test_app_id_try_from_str_invalid_hex() {
3373 let result = AppId::try_from("app-zzz");
3374 assert!(result.is_err());
3375 }
3376
3377 #[test]
3378 fn test_app_id_into_u64() {
3379 let id = AppId::from(321321);
3380 let value: u64 = id.into();
3381 assert_eq!(value, 321321);
3382 }
3383
3384 #[test]
3386 fn test_image_id_from_u64() {
3387 let id = ImageId::from(789789);
3388 assert_eq!(id.value(), 789789);
3389 }
3390
3391 #[test]
3392 fn test_image_id_display() {
3393 let id = ImageId::from(0xabcd1234);
3394 assert_eq!(format!("{}", id), "im-abcd1234");
3395 }
3396
3397 #[test]
3398 fn test_image_id_try_from_str_valid() {
3399 let id = ImageId::try_from("im-abcd1234").unwrap();
3400 assert_eq!(id.value(), 0xabcd1234);
3401 }
3402
3403 #[test]
3404 fn test_image_id_try_from_str_invalid_prefix() {
3405 let result = ImageId::try_from("image-123");
3406 assert!(result.is_err());
3407 match result {
3408 Err(Error::InvalidParameters(msg)) => {
3409 assert!(msg.contains("must start with 'im-'"));
3410 }
3411 _ => panic!("Expected InvalidParameters error"),
3412 }
3413 }
3414
3415 #[test]
3416 fn test_image_id_try_from_str_invalid_hex() {
3417 let result = ImageId::try_from("im-zzz");
3418 assert!(result.is_err());
3419 }
3420
3421 #[test]
3422 fn test_image_id_into_u64() {
3423 let id = ImageId::from(987987);
3424 let value: u64 = id.into();
3425 assert_eq!(value, 987987);
3426 }
3427
3428 #[test]
3430 fn test_id_types_equality() {
3431 let id1 = ProjectID::from(12345);
3432 let id2 = ProjectID::from(12345);
3433 let id3 = ProjectID::from(54321);
3434
3435 assert_eq!(id1, id2);
3436 assert_ne!(id1, id3);
3437 }
3438
3439 #[test]
3440 fn test_id_types_hash() {
3441 use std::collections::HashSet;
3442
3443 let mut set = HashSet::new();
3444 set.insert(DatasetID::from(100));
3445 set.insert(DatasetID::from(200));
3446 set.insert(DatasetID::from(100)); assert_eq!(set.len(), 2);
3449 assert!(set.contains(&DatasetID::from(100)));
3450 assert!(set.contains(&DatasetID::from(200)));
3451 }
3452
3453 #[test]
3454 fn test_id_types_copy_clone() {
3455 let id1 = ExperimentID::from(999);
3456 let id2 = id1; let id3 = id1; assert_eq!(id1, id2);
3460 assert_eq!(id1, id3);
3461 }
3462
3463 #[test]
3465 fn test_id_zero_value() {
3466 let id = ProjectID::from(0);
3467 assert_eq!(format!("{}", id), "p-0");
3468 assert_eq!(id.value(), 0);
3469 }
3470
3471 #[test]
3472 fn test_id_max_value() {
3473 let id = ProjectID::from(u64::MAX);
3474 assert_eq!(format!("{}", id), "p-ffffffffffffffff");
3475 assert_eq!(id.value(), u64::MAX);
3476 }
3477
3478 #[test]
3479 fn test_id_round_trip_conversion() {
3480 let original = 0xdeadbeef_u64;
3481 let id = TrainingSessionID::from(original);
3482 let back: u64 = id.into();
3483 assert_eq!(original, back);
3484 }
3485
3486 #[test]
3487 fn test_id_case_insensitive_hex() {
3488 let id1 = DatasetID::from_str("ds-ABCDEF").unwrap();
3490 let id2 = DatasetID::from_str("ds-abcdef").unwrap();
3491 assert_eq!(id1.value(), id2.value());
3492 }
3493
3494 #[test]
3495 fn test_id_with_leading_zeros() {
3496 let id = ProjectID::from_str("p-00001234").unwrap();
3497 assert_eq!(id.value(), 0x1234);
3498 }
3499
3500 #[test]
3502 fn test_parameter_integer() {
3503 let param = Parameter::Integer(42);
3504 match param {
3505 Parameter::Integer(val) => assert_eq!(val, 42),
3506 _ => panic!("Expected Integer variant"),
3507 }
3508 }
3509
3510 #[test]
3511 fn test_parameter_real() {
3512 let param = Parameter::Real(2.5);
3513 match param {
3514 Parameter::Real(val) => assert_eq!(val, 2.5),
3515 _ => panic!("Expected Real variant"),
3516 }
3517 }
3518
3519 #[test]
3520 fn test_parameter_boolean() {
3521 let param = Parameter::Boolean(true);
3522 match param {
3523 Parameter::Boolean(val) => assert!(val),
3524 _ => panic!("Expected Boolean variant"),
3525 }
3526 }
3527
3528 #[test]
3529 fn test_parameter_string() {
3530 let param = Parameter::String("test".to_string());
3531 match param {
3532 Parameter::String(val) => assert_eq!(val, "test"),
3533 _ => panic!("Expected String variant"),
3534 }
3535 }
3536
3537 #[test]
3538 fn test_parameter_array() {
3539 let param = Parameter::Array(vec![
3540 Parameter::Integer(1),
3541 Parameter::Integer(2),
3542 Parameter::Integer(3),
3543 ]);
3544 match param {
3545 Parameter::Array(arr) => assert_eq!(arr.len(), 3),
3546 _ => panic!("Expected Array variant"),
3547 }
3548 }
3549
3550 #[test]
3551 fn test_parameter_object() {
3552 let mut map = HashMap::new();
3553 map.insert("key".to_string(), Parameter::Integer(100));
3554 let param = Parameter::Object(map);
3555 match param {
3556 Parameter::Object(obj) => {
3557 assert_eq!(obj.len(), 1);
3558 assert!(obj.contains_key("key"));
3559 }
3560 _ => panic!("Expected Object variant"),
3561 }
3562 }
3563
3564 #[test]
3565 fn test_parameter_clone() {
3566 let param1 = Parameter::Integer(42);
3567 let param2 = param1.clone();
3568 assert_eq!(param1, param2);
3569 }
3570
3571 #[test]
3572 fn test_parameter_nested() {
3573 let inner_array = Parameter::Array(vec![Parameter::Integer(1), Parameter::Integer(2)]);
3574 let outer_array = Parameter::Array(vec![inner_array.clone(), inner_array]);
3575
3576 match outer_array {
3577 Parameter::Array(arr) => {
3578 assert_eq!(arr.len(), 2);
3579 }
3580 _ => panic!("Expected Array variant"),
3581 }
3582 }
3583
3584 macro_rules! test_typeid_conversions {
3587 ($test_name:ident, $type:ty, $prefix:literal, $wrong_prefix:literal) => {
3588 #[test]
3589 fn $test_name() {
3590 let id = <$type>::from(0xabc123);
3592 assert_eq!(id.value(), 0xabc123);
3593
3594 assert_eq!(format!("{}", id), concat!($prefix, "-abc123"));
3596
3597 let id: $type = concat!($prefix, "-abc123").parse().unwrap();
3599 assert_eq!(id.value(), 0xabc123);
3600
3601 assert!(concat!($wrong_prefix, "-abc").parse::<$type>().is_err());
3603
3604 assert!("abc123".parse::<$type>().is_err());
3606
3607 assert!(concat!($prefix, "-xyz").parse::<$type>().is_err());
3609
3610 let id = <$type>::try_from(concat!($prefix, "-abc123")).unwrap();
3612 assert_eq!(id.value(), 0xabc123);
3613
3614 let id = <$type>::try_from(concat!($prefix, "-abc123").to_string()).unwrap();
3616 assert_eq!(id.value(), 0xabc123);
3617
3618 let id = <$type>::from(0xabc123);
3620 let json = serde_json::to_string(&id).unwrap();
3621 let parsed: $type = serde_json::from_str(&json).unwrap();
3622 assert_eq!(id, parsed);
3623
3624 let id = <$type>::from(0xabc123);
3626 let val: u64 = id.into();
3627 assert_eq!(val, 0xabc123);
3628 }
3629 };
3630 }
3631
3632 test_typeid_conversions!(test_organization_id_conversions, OrganizationID, "org", "p");
3633 test_typeid_conversions!(test_project_id_conversions, ProjectID, "p", "org");
3634 test_typeid_conversions!(test_experiment_id_conversions, ExperimentID, "exp", "p");
3635 test_typeid_conversions!(
3636 test_training_session_id_conversions,
3637 TrainingSessionID,
3638 "t",
3639 "v"
3640 );
3641 test_typeid_conversions!(
3642 test_validation_session_id_conversions,
3643 ValidationSessionID,
3644 "v",
3645 "t"
3646 );
3647 test_typeid_conversions!(test_snapshot_id_conversions, SnapshotID, "ss", "ds");
3648 test_typeid_conversions!(test_task_id_conversions, TaskID, "task", "t");
3649 test_typeid_conversions!(test_dataset_id_conversions, DatasetID, "ds", "ss");
3650 test_typeid_conversions!(
3651 test_annotation_set_id_conversions,
3652 AnnotationSetID,
3653 "as",
3654 "ds"
3655 );
3656 test_typeid_conversions!(test_sample_id_conversions, SampleID, "s", "p");
3657 test_typeid_conversions!(test_app_id_conversions, AppId, "app", "p");
3658 test_typeid_conversions!(test_image_id_conversions, ImageId, "im", "se");
3659 test_typeid_conversions!(test_sequence_id_conversions, SequenceId, "se", "im");
3660
3661 #[test]
3664 fn test_version_tag_deserialize_full() {
3665 let json = r#"{
3666 "id": 456, "dataset_id": 1715004, "name": "training-v1.0",
3667 "serial": 42, "description": "Ready for production",
3668 "created_by": "user@example.com", "created_at": "2025-01-15T10:30:00Z",
3669 "image_count": 50000, "annotation_counts": {"box": 150000, "seg": 20000},
3670 "sensor_counts": {"lidar": 25000}, "label_count": 15,
3671 "annotation_set_count": 3, "snapshot_id": 789
3672 }"#;
3673 let tag: VersionTag = serde_json::from_str(json).unwrap();
3674 assert_eq!(tag.name(), "training-v1.0");
3675 assert_eq!(tag.serial(), 42);
3676 assert_eq!(tag.image_count(), 50000);
3677 assert_eq!(tag.annotation_counts().get("box"), Some(&150000));
3678 assert_eq!(tag.snapshot_id(), Some(789));
3679 }
3680
3681 #[test]
3682 fn test_version_tag_deserialize_omitempty() {
3683 let json = r#"{
3685 "id": 1, "dataset_id": 2, "name": "v1.0", "serial": 5,
3686 "description": "", "created_by": "user",
3687 "created_at": "2025-01-01T00:00:00Z"
3688 }"#;
3689 let tag: VersionTag = serde_json::from_str(json).unwrap();
3690 assert_eq!(tag.snapshot_id(), None);
3691 assert_eq!(tag.image_count(), 0);
3692 assert!(tag.annotation_counts().is_empty());
3693 }
3694
3695 #[test]
3696 fn test_changelog_entry_deserialize_omitempty() {
3697 let json = r#"{
3699 "id": 1, "dataset_id": 2, "serial": 3, "entity_type": "image",
3700 "operation": "bulk_create", "change_data": {"count": 5},
3701 "username": "user", "organization_id": 1,
3702 "created_at": "2025-01-01T00:00:00Z", "message": ""
3703 }"#;
3704 let entry: ChangelogEntry = serde_json::from_str(json).unwrap();
3705 assert!(entry.entity_id().is_none());
3706 assert!(entry.s3_version_ids().is_empty());
3707 assert_eq!(entry.entity_type(), "image");
3708 assert_eq!(entry.operation(), "bulk_create");
3709 }
3710
3711 #[test]
3712 fn test_changelog_response_deserialize() {
3713 let json = r#"{
3714 "entries": [], "count": 0, "continue_token": ""
3715 }"#;
3716 let resp: ChangelogResponse = serde_json::from_str(json).unwrap();
3717 assert!(resp.entries.is_empty());
3718 assert_eq!(resp.count, 0);
3719 assert!(resp.continue_token.is_empty());
3720 assert!(resp.from_serial.is_none());
3721 }
3722
3723 #[test]
3724 fn test_version_current_no_latest_tag() {
3725 let json = r#"{
3727 "dataset_id": 100, "current_serial": 5, "tags": []
3728 }"#;
3729 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3730 assert!(resp.latest_tag.is_none());
3731 assert!(resp.tags.is_empty());
3732 assert_eq!(resp.current_serial, 5);
3733 }
3734
3735 #[test]
3736 fn test_version_current_with_latest_tag() {
3737 let json = r#"{
3738 "dataset_id": 100, "current_serial": 42,
3739 "latest_tag": {
3740 "id": 1, "dataset_id": 100, "name": "v1.0", "serial": 42,
3741 "description": "test", "created_by": "user",
3742 "created_at": "2025-01-01T00:00:00Z",
3743 "image_count": 10, "label_count": 2, "annotation_set_count": 1
3744 },
3745 "tags": []
3746 }"#;
3747 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3748 assert!(resp.latest_tag.is_some());
3749 assert_eq!(resp.latest_tag.unwrap().name(), "v1.0");
3750 }
3751
3752 #[test]
3753 fn test_version_tag_is_current_field() {
3754 let json = r#"{
3755 "id": 1, "dataset_id": 5, "name": "v1.0", "serial": 10,
3756 "created_by": "alice", "created_at": "2026-01-01T00:00:00Z",
3757 "is_current": true
3758 }"#;
3759 let tag: VersionTag = serde_json::from_str(json).unwrap();
3760 assert!(tag.is_current());
3761 }
3762
3763 #[test]
3764 fn test_version_tag_is_current_false() {
3765 let json = r#"{
3766 "id": 1, "dataset_id": 5, "name": "v1.0", "serial": 10,
3767 "created_by": "alice", "created_at": "2026-01-01T00:00:00Z",
3768 "is_current": false
3769 }"#;
3770 let tag: VersionTag = serde_json::from_str(json).unwrap();
3771 assert!(!tag.is_current());
3772 }
3773
3774 #[test]
3775 fn test_dataset_summary_deserialize() {
3776 let json = r#"{
3777 "dataset_id": 100, "current_serial": 10,
3778 "image_count": 5000, "annotation_counts": {"box": 10000},
3779 "sensor_counts": {}, "label_count": 8,
3780 "annotation_set_count": 2, "last_updated": "2025-06-01T12:00:00Z"
3781 }"#;
3782 let summary: DatasetSummary = serde_json::from_str(json).unwrap();
3783 assert_eq!(summary.image_count(), 5000);
3784 assert_eq!(summary.label_count(), 8);
3785 assert_eq!(summary.annotation_counts().get("box"), Some(&10000));
3786 }
3787
3788 #[test]
3789 fn test_restore_result_deserialize() {
3790 let json = r#"{
3791 "success": true, "new_serial": 45,
3792 "restored_from": {"tag": "v1.0", "serial": 42},
3793 "restored_counts": {"images": 5000, "labels": 15, "annotation_sets": 3},
3794 "message": "Dataset restored to tag v1.0"
3795 }"#;
3796 let result: RestoreResult = serde_json::from_str(json).unwrap();
3797 assert!(result.success);
3798 assert_eq!(result.new_serial, 45);
3799 assert_eq!(result.restored_from.tag, "v1.0");
3800 assert_eq!(result.restored_from.serial, 42);
3801 assert_eq!(result.restored_counts.images, 5000);
3802 }
3803
3804 #[test]
3805 fn test_sample_delete_params_serializes_all_fields() {
3806 let params = SampleDeleteParams {
3812 dataset_id: 42,
3813 image_ids: vec![1, 2, 3],
3814 sequence_ids: Vec::new(),
3815 delete_all: false,
3816 };
3817 let value = serde_json::to_value(¶ms).unwrap();
3818 let obj = value.as_object().unwrap();
3819 assert_eq!(obj.len(), 4);
3820 assert_eq!(obj["dataset_id"], serde_json::json!(42));
3821 assert_eq!(obj["image_ids"], serde_json::json!([1, 2, 3]));
3822 assert_eq!(obj["sequence_ids"], serde_json::json!([]));
3823 assert_eq!(obj["delete_all"], serde_json::json!(false));
3824 }
3825}
3826
3827#[cfg(test)]
3828mod tests_task_data_list {
3829 use super::*;
3830
3831 #[test]
3832 fn task_data_list_deserializes_from_server_shape() {
3833 let json = r#"{
3834 "server": "test.edgefirst.studio",
3835 "organization_uid": "org-abc123",
3836 "traces": ["trace/imx95.json"],
3837 "data": {
3838 "predictions": ["predictions.parquet"],
3839 "trace": ["imx95.json"]
3840 }
3841 }"#;
3842 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
3843 assert_eq!(parsed.server, "test.edgefirst.studio");
3844 assert_eq!(parsed.organization_uid, "org-abc123");
3845 assert_eq!(parsed.traces, vec!["trace/imx95.json"]);
3846 assert_eq!(
3847 parsed.data.get("predictions").unwrap(),
3848 &vec!["predictions.parquet".to_string()]
3849 );
3850 }
3851}
3852
3853#[cfg(test)]
3854mod tests_upload_data {
3855 #[test]
3859 fn folder_empty_string_is_normalised() {
3860 let folder: Option<&str> = Some("");
3861 assert!(folder.filter(|s| !s.is_empty()).is_none());
3862
3863 let folder_real: Option<&str> = Some("predictions");
3864 assert!(folder_real.filter(|s| !s.is_empty()).is_some());
3865 }
3866}
3867
3868#[cfg(test)]
3869mod tests_job_struct {
3870 use super::*;
3871
3872 #[test]
3873 fn job_deserializes_with_all_fields() {
3874 let json = r#"{
3875 "code": "edgefirst-validator:2.9.5",
3876 "title": "EdgeFirst Validator",
3877 "job_name": "smoke-test",
3878 "job_id": "aws-batch-abc",
3879 "state": "RUNNING",
3880 "launch": "2026-05-14T15:00:00Z",
3881 "task_id": 6789
3882 }"#;
3883 let job: Job = serde_json::from_str(json).unwrap();
3884 assert_eq!(job.code, "edgefirst-validator:2.9.5");
3885 assert_eq!(job.title, "EdgeFirst Validator");
3886 assert_eq!(job.job_name, "smoke-test");
3887 assert_eq!(job.job_id, "aws-batch-abc");
3888 assert_eq!(job.state, "RUNNING");
3889 assert!(job.launch.is_some());
3890 assert_eq!(job.task_id, 6789);
3891 }
3892
3893 #[test]
3894 fn job_tolerates_missing_optional_fields() {
3895 let json = r#"{ "task_id": 42 }"#;
3899 let job: Job = serde_json::from_str(json).unwrap();
3900 assert_eq!(job.task_id, 42);
3901 assert!(job.code.is_empty());
3902 assert!(job.title.is_empty());
3903 assert!(job.job_name.is_empty());
3904 assert!(job.job_id.is_empty());
3905 assert!(job.state.is_empty());
3906 assert!(job.launch.is_none());
3907 }
3908
3909 #[test]
3910 fn job_task_id_accessor_saturates_negative_to_zero() {
3911 let job = Job {
3916 code: String::new(),
3917 title: String::new(),
3918 job_name: String::new(),
3919 job_id: String::new(),
3920 state: String::new(),
3921 launch: None,
3922 task_id: -1,
3923 };
3924 assert_eq!(job.task_id().value(), 0);
3925 }
3926
3927 #[test]
3928 fn job_task_id_accessor_passes_through_positive_values() {
3929 let job = Job {
3930 code: String::new(),
3931 title: String::new(),
3932 job_name: String::new(),
3933 job_id: String::new(),
3934 state: String::new(),
3935 launch: None,
3936 task_id: 12345,
3937 };
3938 assert_eq!(job.task_id().value(), 12345);
3939 }
3940
3941 #[test]
3942 fn job_ignores_unknown_fields() {
3943 let json = r#"{
3947 "code": "x",
3948 "task_id": 1,
3949 "docker_task": { "image": "x" },
3950 "aws_region": "us-east-1",
3951 "tags": ["a", "b"]
3952 }"#;
3953 let job: Job = serde_json::from_str(json).unwrap();
3954 assert_eq!(job.task_id, 1);
3955 }
3956}
3957
3958#[cfg(test)]
3959mod tests_task_info_schema_tolerance {
3960 use super::*;
3961
3962 #[test]
3967 fn task_info_accepts_task_description_field() {
3968 let json = r#"{
3970 "id": 6699,
3971 "type": "edgefirst-validator:2.9.5",
3972 "task_description": "Profiler run for IMX95",
3973 "status": "running"
3974 }"#;
3975 let info: TaskInfo = serde_json::from_str(json).unwrap();
3976 assert_eq!(info.description(), "Profiler run for IMX95");
3977 }
3978
3979 #[test]
3980 fn task_info_accepts_legacy_description_field() {
3981 let json = r#"{
3983 "id": 6699,
3984 "type": "edgefirst-validator:2.9.5",
3985 "description": "Legacy description"
3986 }"#;
3987 let info: TaskInfo = serde_json::from_str(json).unwrap();
3988 assert_eq!(info.description(), "Legacy description");
3989 }
3990
3991 #[test]
3992 fn task_info_tolerates_missing_description() {
3993 let json = r#"{
3995 "id": 6699,
3996 "type": "x"
3997 }"#;
3998 let info: TaskInfo = serde_json::from_str(json).unwrap();
3999 assert!(info.description().is_empty());
4000 }
4001
4002 #[test]
4003 fn task_info_tolerates_missing_dates_via_default() {
4004 let json = r#"{
4006 "id": 6699,
4007 "type": "x"
4008 }"#;
4009 let info: TaskInfo = serde_json::from_str(json).unwrap();
4010 assert_eq!(info.id().value(), 6699);
4012 }
4013
4014 #[test]
4015 fn task_info_status_accessor_returns_option() {
4016 let json = r#"{
4017 "id": 1,
4018 "type": "x"
4019 }"#;
4020 let info: TaskInfo = serde_json::from_str(json).unwrap();
4021 assert!(info.status().is_none());
4022 }
4023
4024 #[test]
4025 fn task_info_stages_returns_empty_map_when_unset() {
4026 let json = r#"{
4027 "id": 1,
4028 "type": "x"
4029 }"#;
4030 let info: TaskInfo = serde_json::from_str(json).unwrap();
4031 let stages = info.stages();
4032 assert!(stages.is_empty());
4033 }
4034}
4035
4036#[cfg(test)]
4037mod tests_stage_struct {
4038 use super::*;
4039
4040 #[test]
4041 fn stage_new_sets_only_supplied_fields() {
4042 let stage = Stage::new(
4043 None,
4044 "download".into(),
4045 Some("running".into()),
4046 Some("fetching".into()),
4047 42,
4048 );
4049 assert!(stage.task_id().is_none());
4050 assert_eq!(stage.stage(), "download");
4051 assert_eq!(stage.status().as_deref(), Some("running"));
4052 assert_eq!(stage.message().as_deref(), Some("fetching"));
4053 assert_eq!(stage.percentage(), 42);
4054 assert!(stage.description().is_none());
4056 }
4057
4058 #[test]
4059 fn stage_serializes_without_optional_none_fields() {
4060 let stage = Stage::new(None, "init".into(), None, None, 0);
4062 let json = serde_json::to_value(&stage).unwrap();
4063 assert!(json.get("status").is_none(), "got: {json}");
4064 assert!(json.get("message").is_none(), "got: {json}");
4065 assert!(json.get("docker_task_id").is_none(), "got: {json}");
4066 assert_eq!(json["stage"], "init");
4068 assert_eq!(json["percentage"], 0);
4069 }
4070
4071 #[test]
4072 fn stage_serializes_task_id_when_present() {
4073 let task_id = TaskID::from(0xdeadu64);
4074 let stage = Stage::new(Some(task_id), "x".into(), None, None, 0);
4075 let json = serde_json::to_value(&stage).unwrap();
4076 assert!(json.get("docker_task_id").is_some());
4079 }
4080
4081 #[test]
4082 fn stage_round_trips_through_json() {
4083 let stage = Stage::new(
4084 None,
4085 "train".into(),
4086 Some("done".into()),
4087 Some("epoch 100".into()),
4088 100,
4089 );
4090 let s = serde_json::to_string(&stage).unwrap();
4091 let back: Stage = serde_json::from_str(&s).unwrap();
4092 assert_eq!(back.stage(), "train");
4093 assert_eq!(back.status().as_deref(), Some("done"));
4094 assert_eq!(back.message().as_deref(), Some("epoch 100"));
4095 assert_eq!(back.percentage(), 100);
4096 }
4097}
4098
4099#[cfg(test)]
4100mod tests_task_data_list_extra {
4101 use super::*;
4102
4103 #[test]
4104 fn task_data_list_with_empty_data_map() {
4105 let json = r#"{
4106 "server": "studio",
4107 "organization_uid": "org-1",
4108 "traces": [],
4109 "data": {}
4110 }"#;
4111 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4112 assert!(parsed.traces.is_empty());
4113 assert!(parsed.data.is_empty());
4114 }
4115
4116 #[test]
4117 fn task_data_list_multiple_folders() {
4118 let json = r#"{
4119 "server": "studio",
4120 "organization_uid": "org-1",
4121 "traces": ["t1", "t2"],
4122 "data": {
4123 "predictions": ["a.parquet", "b.parquet"],
4124 "metrics": ["loss.json"]
4125 }
4126 }"#;
4127 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4128 assert_eq!(parsed.traces.len(), 2);
4129 assert_eq!(parsed.data.len(), 2);
4130 assert_eq!(parsed.data["predictions"].len(), 2);
4131 }
4132}
4133
4134#[cfg(test)]
4135mod tests_artifact_struct {
4136 use super::*;
4137
4138 #[test]
4139 fn artifact_accessors_return_strs() {
4140 let json = r#"{ "name": "best.onnx", "modelType": "yolo" }"#;
4143 let a: Artifact = serde_json::from_str(json).unwrap();
4144 assert_eq!(a.name(), "best.onnx");
4145 assert_eq!(a.model_type(), "yolo");
4146 }
4147}
4148
4149#[cfg(test)]
4150mod tests_task_status_serialize {
4151 use super::*;
4152
4153 #[test]
4154 fn task_status_uses_docker_task_id_wire_field() {
4155 let s = TaskStatus {
4156 task_id: TaskID::from(0x1a2bu64),
4157 status: "training".into(),
4158 };
4159 let json = serde_json::to_value(&s).unwrap();
4160 assert!(json.get("docker_task_id").is_some(), "got: {json}");
4162 assert_eq!(json["status"], "training");
4163 }
4164}
4165
4166#[cfg(test)]
4167mod tests_task_stages_serialize {
4168 use super::*;
4169
4170 #[test]
4171 fn task_stages_omits_empty_vec() {
4172 let stages = TaskStages {
4173 task_id: TaskID::from(1u64),
4174 stages: Vec::new(),
4175 };
4176 let json = serde_json::to_value(&stages).unwrap();
4177 assert!(json.get("stages").is_none(), "got: {json}");
4179 }
4180
4181 #[test]
4182 fn task_stages_serializes_non_empty_vec() {
4183 let stages = TaskStages {
4184 task_id: TaskID::from(1u64),
4185 stages: vec![std::collections::HashMap::from([(
4186 "stage".to_string(),
4187 "download".to_string(),
4188 )])],
4189 };
4190 let json = serde_json::to_value(&stages).unwrap();
4191 assert_eq!(json["stages"][0]["stage"], "download");
4192 }
4193}