1use crate::{AnnotationSet, Client, Dataset, Error, Progress, Sample, client};
5use chrono::{DateTime, Utc};
6use log::trace;
7use reqwest::multipart::{Form, Part};
8use serde::{Deserialize, Deserializer, Serialize};
9use std::{collections::HashMap, fmt::Display, path::PathBuf, str::FromStr};
10
11fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
16where
17 D: Deserializer<'de>,
18 T: Default + Deserialize<'de>,
19{
20 Ok(Option::deserialize(deserializer)?.unwrap_or_default())
21}
22
23#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
53#[serde(untagged)]
54pub enum Parameter {
55 Integer(i64),
57 Real(f64),
59 Boolean(bool),
61 String(String),
63 Array(Vec<Parameter>),
65 Object(HashMap<String, Parameter>),
67}
68
69#[derive(Deserialize)]
70pub struct LoginResult {
71 pub(crate) token: String,
72}
73
74macro_rules! typeid {
83 ($(#[$meta:meta])* $name:ident, $prefix:literal) => {
84 $(#[$meta])*
85 #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
86 pub struct $name(u64);
87
88 impl Display for $name {
89 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
90 write!(f, concat!($prefix, "-{:x}"), self.0)
91 }
92 }
93
94 impl From<u64> for $name {
95 fn from(id: u64) -> Self {
96 $name(id)
97 }
98 }
99
100 impl From<$name> for u64 {
101 fn from(val: $name) -> Self {
102 val.0
103 }
104 }
105
106 impl $name {
107 pub fn value(&self) -> u64 {
109 self.0
110 }
111 }
112
113 impl TryFrom<&str> for $name {
114 type Error = Error;
115
116 fn try_from(s: &str) -> Result<Self, Self::Error> {
117 $name::from_str(s)
118 }
119 }
120
121 impl TryFrom<String> for $name {
122 type Error = Error;
123
124 fn try_from(s: String) -> Result<Self, Self::Error> {
125 $name::from_str(&s)
126 }
127 }
128
129 impl FromStr for $name {
130 type Err = Error;
131
132 fn from_str(s: &str) -> Result<Self, Self::Err> {
133 let hex_part =
134 s.strip_prefix(concat!($prefix, "-")).ok_or_else(|| {
135 Error::InvalidParameters(format!(
136 "{} must start with '{}-' prefix",
137 stringify!($name),
138 $prefix
139 ))
140 })?;
141 let id = u64::from_str_radix(hex_part, 16)?;
142 Ok($name(id))
143 }
144 }
145 };
146}
147
148typeid!(
149 OrganizationID,
169 "org"
170);
171
172#[derive(Deserialize, Clone, Debug)]
193pub struct Organization {
194 id: OrganizationID,
195 name: String,
196 #[serde(rename = "latest_credit")]
197 credits: i64,
198}
199
200impl Display for Organization {
201 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
202 write!(f, "{}", self.name())
203 }
204}
205
206impl Organization {
207 pub fn id(&self) -> OrganizationID {
208 self.id
209 }
210
211 pub fn name(&self) -> &str {
212 &self.name
213 }
214
215 pub fn credits(&self) -> i64 {
216 self.credits
217 }
218}
219
220#[derive(Deserialize, Clone, Debug)]
226pub struct UsageSummary {
227 #[serde(default)]
228 credits: f64,
229 #[serde(default)]
230 funds: f64,
231 #[serde(default, rename = "total_funds_and_credits")]
232 total: f64,
233}
234
235impl UsageSummary {
236 pub fn credits(&self) -> f64 {
237 self.credits
238 }
239
240 pub fn funds(&self) -> f64 {
241 self.funds
242 }
243
244 pub fn total(&self) -> f64 {
245 self.total
246 }
247}
248
249typeid!(
250 ProjectID,
271 "p"
272);
273
274typeid!(
275 ExperimentID,
296 "exp"
297);
298
299typeid!(
300 TrainingSessionID,
321 "t"
322);
323
324typeid!(
325 ValidationSessionID,
345 "v"
346);
347
348typeid!(
349 SnapshotID,
365 "ss"
366);
367
368typeid!(
369 TaskID,
385 "task"
386);
387
388typeid!(
389 DatasetID,
410 "ds"
411);
412
413typeid!(
414 AnnotationSetID,
430 "as"
431);
432
433typeid!(
434 SampleID,
450 "s"
451);
452
453typeid!(
454 AppId,
460 "app"
461);
462
463typeid!(
464 ImageId,
470 "im"
471);
472
473typeid!(
474 SequenceId,
480 "se"
481);
482
483#[derive(Deserialize, Clone, Debug)]
487pub struct Project {
488 id: ProjectID,
489 name: String,
490 description: String,
491}
492
493impl Display for Project {
494 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
495 write!(f, "{} {}", self.id(), self.name())
496 }
497}
498
499impl Project {
500 pub fn id(&self) -> ProjectID {
501 self.id
502 }
503
504 pub fn name(&self) -> &str {
505 &self.name
506 }
507
508 pub fn description(&self) -> &str {
509 &self.description
510 }
511
512 pub async fn datasets(
513 &self,
514 client: &client::Client,
515 name: Option<&str>,
516 ) -> Result<Vec<Dataset>, Error> {
517 client.datasets(self.id, name).await
518 }
519
520 pub async fn experiments(
521 &self,
522 client: &client::Client,
523 name: Option<&str>,
524 ) -> Result<Vec<Experiment>, Error> {
525 client.experiments(self.id, name).await
526 }
527}
528
529#[derive(Deserialize, Debug)]
530pub struct SamplesCountResult {
531 pub total: u64,
532}
533
534#[derive(Serialize, Clone, Debug)]
535pub struct SamplesListParams {
536 pub dataset_id: DatasetID,
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub annotation_set_id: Option<AnnotationSetID>,
539 #[serde(skip_serializing_if = "Option::is_none")]
540 pub continue_token: Option<String>,
541 #[serde(skip_serializing_if = "Vec::is_empty")]
542 pub types: Vec<String>,
543 #[serde(skip_serializing_if = "Vec::is_empty")]
544 pub group_names: Vec<String>,
545 #[serde(skip_serializing_if = "Option::is_none")]
546 pub tag: Option<String>,
547 #[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 model_params: HashMap<String, Parameter>,
860 dataset_params: DatasetParams,
861}
862
863#[derive(Deserialize)]
864pub struct TrainingSession {
865 id: TrainingSessionID,
866 #[serde(rename = "trainer_id")]
867 experiment_id: ExperimentID,
868 model: String,
869 name: String,
870 description: String,
871 params: TrainingSessionParams,
872 #[serde(rename = "docker_task")]
873 task: Task,
874}
875
876impl Display for TrainingSession {
877 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
878 write!(f, "{} {}", self.id, self.name())
879 }
880}
881
882impl TrainingSession {
883 pub fn id(&self) -> TrainingSessionID {
884 self.id
885 }
886
887 pub fn name(&self) -> &str {
888 &self.name
889 }
890
891 pub fn description(&self) -> &str {
892 &self.description
893 }
894
895 pub fn model(&self) -> &str {
896 &self.model
897 }
898
899 pub fn experiment_id(&self) -> ExperimentID {
900 self.experiment_id
901 }
902
903 pub fn task(&self) -> Task {
904 self.task.clone()
905 }
906
907 pub fn model_params(&self) -> &HashMap<String, Parameter> {
908 &self.params.model_params
909 }
910
911 pub fn dataset_params(&self) -> &DatasetParams {
912 &self.params.dataset_params
913 }
914
915 pub fn train_group(&self) -> &str {
916 &self.params.dataset_params.train_group
917 }
918
919 pub fn val_group(&self) -> &str {
920 &self.params.dataset_params.val_group
921 }
922
923 pub async fn experiment(&self, client: &client::Client) -> Result<Experiment, Error> {
924 client.experiment(self.experiment_id).await
925 }
926
927 pub async fn dataset(&self, client: &client::Client) -> Result<Dataset, Error> {
928 client.dataset(self.params.dataset_params.dataset_id).await
929 }
930
931 pub async fn annotation_set(&self, client: &client::Client) -> Result<AnnotationSet, Error> {
932 client
933 .annotation_set(self.params.dataset_params.annotation_set_id)
934 .await
935 }
936
937 pub async fn artifacts(&self, client: &client::Client) -> Result<Vec<Artifact>, Error> {
938 client.artifacts(self.id).await
939 }
940
941 pub async fn metrics(
942 &self,
943 client: &client::Client,
944 ) -> Result<HashMap<String, Parameter>, Error> {
945 #[derive(Deserialize)]
946 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
947 enum Response {
948 Empty {},
949 Map(HashMap<String, Parameter>),
950 String(String),
951 }
952
953 let params = HashMap::from([("trainer_session_id", self.id().value())]);
954 let resp: Response = client
955 .rpc("trainer.session.metrics".to_owned(), Some(params))
956 .await?;
957
958 Ok(match resp {
959 Response::String(metrics) => serde_json::from_str(&metrics)?,
960 Response::Map(metrics) => metrics,
961 Response::Empty {} => HashMap::new(),
962 })
963 }
964
965 pub async fn set_metrics(
966 &self,
967 client: &client::Client,
968 metrics: HashMap<String, Parameter>,
969 ) -> Result<(), Error> {
970 let metrics = PublishMetrics {
971 trainer_session_id: Some(self.id()),
972 validate_session_id: None,
973 metrics,
974 };
975
976 let _: String = client
977 .rpc("trainer.session.metrics".to_owned(), Some(metrics))
978 .await?;
979
980 Ok(())
981 }
982
983 pub async fn download_artifact(
985 &self,
986 client: &client::Client,
987 filename: &str,
988 ) -> Result<Vec<u8>, Error> {
989 client
990 .fetch(&format!(
991 "download_model?training_session_id={}&file={}",
992 self.id().value(),
993 filename
994 ))
995 .await
996 }
997
998 pub async fn upload_artifact(
1002 &self,
1003 client: &client::Client,
1004 filename: &str,
1005 path: PathBuf,
1006 ) -> Result<(), Error> {
1007 self.upload(client, &[(format!("artifacts/{}", filename), path)])
1008 .await
1009 }
1010
1011 pub async fn download_checkpoint(
1013 &self,
1014 client: &client::Client,
1015 filename: &str,
1016 ) -> Result<Vec<u8>, Error> {
1017 client
1018 .fetch(&format!(
1019 "download_checkpoint?folder=checkpoints&training_session_id={}&file={}",
1020 self.id().value(),
1021 filename
1022 ))
1023 .await
1024 }
1025
1026 pub async fn upload_checkpoint(
1030 &self,
1031 client: &client::Client,
1032 filename: &str,
1033 path: PathBuf,
1034 ) -> Result<(), Error> {
1035 self.upload(client, &[(format!("checkpoints/{}", filename), path)])
1036 .await
1037 }
1038
1039 pub async fn download(&self, client: &client::Client, filename: &str) -> Result<String, Error> {
1043 #[derive(Serialize)]
1044 struct DownloadRequest {
1045 session_id: TrainingSessionID,
1046 file_path: String,
1047 }
1048
1049 let params = DownloadRequest {
1050 session_id: self.id(),
1051 file_path: filename.to_string(),
1052 };
1053
1054 client
1055 .rpc("trainer.download.file".to_owned(), Some(params))
1056 .await
1057 }
1058
1059 pub async fn upload(
1060 &self,
1061 client: &client::Client,
1062 files: &[(String, PathBuf)],
1063 ) -> Result<(), Error> {
1064 let mut parts = Form::new().part(
1065 "params",
1066 Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1067 );
1068
1069 for (name, path) in files {
1070 let file_part = Part::file(path).await?.file_name(name.to_owned());
1071 parts = parts.part("file", file_part);
1072 }
1073
1074 let result = client.post_multipart("trainer.upload.files", parts).await?;
1075 trace!("TrainingSession::upload: {:?}", result);
1076 Ok(())
1077 }
1078}
1079
1080#[derive(Deserialize, Clone, Debug)]
1081pub struct ValidationSession {
1082 id: ValidationSessionID,
1083 description: String,
1084 dataset_id: DatasetID,
1085 experiment_id: ExperimentID,
1086 training_session_id: TrainingSessionID,
1087 #[serde(rename = "gt_annotation_set_id")]
1088 annotation_set_id: AnnotationSetID,
1089 #[serde(deserialize_with = "validation_session_params")]
1090 params: HashMap<String, Parameter>,
1091 #[serde(rename = "docker_task")]
1092 task: Task,
1093}
1094
1095fn validation_session_params<'de, D>(
1096 deserializer: D,
1097) -> Result<HashMap<String, Parameter>, D::Error>
1098where
1099 D: Deserializer<'de>,
1100{
1101 #[derive(Deserialize)]
1102 struct ModelParams {
1103 validation: Option<HashMap<String, Parameter>>,
1104 }
1105
1106 #[derive(Deserialize)]
1107 struct ValidateParams {
1108 model: String,
1109 }
1110
1111 #[derive(Deserialize)]
1112 struct Params {
1113 model_params: ModelParams,
1114 validate_params: ValidateParams,
1115 }
1116
1117 let params = Params::deserialize(deserializer)?;
1118 let params = match params.model_params.validation {
1119 Some(mut map) => {
1120 map.insert(
1121 "model".to_string(),
1122 Parameter::String(params.validate_params.model),
1123 );
1124 map
1125 }
1126 None => HashMap::from([(
1127 "model".to_string(),
1128 Parameter::String(params.validate_params.model),
1129 )]),
1130 };
1131
1132 Ok(params)
1133}
1134
1135impl ValidationSession {
1136 pub fn id(&self) -> ValidationSessionID {
1137 self.id
1138 }
1139
1140 pub fn name(&self) -> &str {
1141 self.task.name()
1142 }
1143
1144 pub fn description(&self) -> &str {
1145 &self.description
1146 }
1147
1148 pub fn dataset_id(&self) -> DatasetID {
1149 self.dataset_id
1150 }
1151
1152 pub fn experiment_id(&self) -> ExperimentID {
1153 self.experiment_id
1154 }
1155
1156 pub fn training_session_id(&self) -> TrainingSessionID {
1157 self.training_session_id
1158 }
1159
1160 pub fn annotation_set_id(&self) -> AnnotationSetID {
1161 self.annotation_set_id
1162 }
1163
1164 pub fn params(&self) -> &HashMap<String, Parameter> {
1165 &self.params
1166 }
1167
1168 pub fn task(&self) -> &Task {
1169 &self.task
1170 }
1171
1172 pub async fn metrics(
1173 &self,
1174 client: &client::Client,
1175 ) -> Result<HashMap<String, Parameter>, Error> {
1176 #[derive(Deserialize)]
1177 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1178 enum Response {
1179 Empty {},
1180 Map(HashMap<String, Parameter>),
1181 String(String),
1182 }
1183
1184 let params = HashMap::from([("validate_session_id", self.id().value())]);
1185 let resp: Response = client
1186 .rpc("validate.session.metrics".to_owned(), Some(params))
1187 .await?;
1188
1189 Ok(match resp {
1190 Response::String(metrics) => serde_json::from_str(&metrics)?,
1191 Response::Map(metrics) => metrics,
1192 Response::Empty {} => HashMap::new(),
1193 })
1194 }
1195
1196 pub async fn set_metrics(
1197 &self,
1198 client: &client::Client,
1199 metrics: HashMap<String, Parameter>,
1200 ) -> Result<(), Error> {
1201 let metrics = PublishMetrics {
1202 trainer_session_id: None,
1203 validate_session_id: Some(self.id()),
1204 metrics,
1205 };
1206
1207 let _: String = client
1208 .rpc("validate.session.metrics".to_owned(), Some(metrics))
1209 .await?;
1210
1211 Ok(())
1212 }
1213
1214 pub async fn upload_data(
1239 &self,
1240 client: &client::Client,
1241 files: &[(String, std::path::PathBuf)],
1242 folder: Option<&str>,
1243 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1244 ) -> Result<(), Error> {
1245 use futures::StreamExt;
1246 use std::sync::{
1247 Arc,
1248 atomic::{AtomicUsize, Ordering},
1249 };
1250 use tokio_util::io::ReaderStream;
1251
1252 let mut total: usize = 0;
1254 let mut file_meta = Vec::with_capacity(files.len());
1255 for (name, path) in files {
1256 let f = tokio::fs::File::open(path).await?;
1257 let len = f.metadata().await?.len() as usize;
1258 total += len;
1259 file_meta.push((name.clone(), f, len));
1260 }
1261
1262 let sent = Arc::new(AtomicUsize::new(0));
1264
1265 let mut form = Form::new().text("session_id", self.id().value().to_string());
1266 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
1267 form = form.text("folder", folder.to_owned());
1268 }
1269
1270 for (name, file, len) in file_meta {
1271 let reader_stream = ReaderStream::new(file);
1272 let sent_clone = sent.clone();
1273 let progress_clone = progress.clone();
1274 let progress_stream = reader_stream.inspect(move |chunk_result| {
1275 if let Ok(chunk) = chunk_result {
1276 let current =
1277 sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1278 if let Some(tx) = &progress_clone {
1283 let _ = tx.try_send(Progress {
1284 current,
1285 total,
1286 status: None,
1287 });
1288 }
1289 }
1290 });
1291 let body = reqwest::Body::wrap_stream(progress_stream);
1292 let part = Part::stream_with_length(body, len as u64).file_name(name);
1293 form = form.part("file", part);
1294 }
1295
1296 let result = match client.post_multipart("val.data.upload", form).await {
1297 Ok(_) => Ok(()),
1298 Err(Error::RpcError(code, msg)) => {
1299 Err(client::map_rpc_error("val.data.upload", code, msg, None))
1300 }
1301 Err(e) => Err(e),
1302 };
1303
1304 if result.is_ok()
1309 && let Some(tx) = progress
1310 {
1311 let _ = tx
1312 .send(Progress {
1313 current: total,
1314 total,
1315 status: None,
1316 })
1317 .await;
1318 }
1319 result
1320 }
1321
1322 pub async fn download_data(
1342 &self,
1343 client: &client::Client,
1344 filename: &str,
1345 output_path: &std::path::Path,
1346 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1347 ) -> Result<(), Error> {
1348 let req = client::ValDataDownloadRequest {
1349 session_id: self.id().value(),
1350 filename: filename.to_owned(),
1351 };
1352 match client
1353 .rpc_download("val.data.download", &req, output_path, progress)
1354 .await
1355 {
1356 Ok(()) => Ok(()),
1357 Err(Error::RpcError(code, msg)) => {
1358 Err(client::map_rpc_error("val.data.download", code, msg, None))
1359 }
1360 Err(e) => Err(e),
1361 }
1362 }
1363
1364 pub async fn data_list(&self, client: &client::Client) -> Result<Vec<String>, Error> {
1379 let req = client::ValDataListRequest {
1380 session_id: self.id().value(),
1381 };
1382 match client.rpc("val.data.list".to_owned(), Some(&req)).await {
1383 Ok(r) => Ok(r),
1384 Err(Error::RpcError(code, msg)) => {
1385 Err(client::map_rpc_error("val.data.list", code, msg, None))
1386 }
1387 Err(e) => Err(e),
1388 }
1389 }
1390}
1391
1392#[derive(Debug, Clone)]
1411pub struct StartValidationRequest {
1412 pub project_id: ProjectID,
1413 pub name: String,
1414 pub training_session_id: TrainingSessionID,
1415 pub model_file: String,
1416 pub val_type: String,
1417 pub params: HashMap<String, Parameter>,
1418 pub is_local: bool,
1419 pub is_kubernetes: bool,
1420 pub description: Option<String>,
1421 pub dataset_id: Option<DatasetID>,
1422 pub annotation_set_id: Option<AnnotationSetID>,
1423 pub snapshot_id: Option<SnapshotID>,
1424}
1425
1426#[derive(Deserialize, Debug, Clone)]
1441pub struct NewValidationSession {
1442 #[serde(rename = "id")]
1443 pub task_id: TaskID,
1444 #[serde(rename = "val_session_id", default)]
1445 pub session_id: Option<ValidationSessionID>,
1446}
1447
1448impl Display for NewValidationSession {
1449 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1450 match self.session_id {
1451 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1452 None => write!(f, "task {} (no session)", self.task_id),
1453 }
1454 }
1455}
1456
1457#[derive(Debug, Clone)]
1477pub struct StartTrainingRequest {
1478 pub project_id: ProjectID,
1480 pub name: String,
1482 pub experiment_id: ExperimentID,
1484 pub trainer_type: String,
1487 pub dataset_id: DatasetID,
1489 pub annotation_set_id: AnnotationSetID,
1491 pub tag_name: Option<String>,
1495 pub train_group: Option<String>,
1497 pub val_group: Option<String>,
1499 pub session_name: Option<String>,
1502 pub session_description: Option<String>,
1504 pub weights_session: Option<TrainingSessionID>,
1506 pub params: HashMap<String, Parameter>,
1508 pub is_local: bool,
1510 pub is_kubernetes: bool,
1512}
1513
1514#[derive(Deserialize, Debug, Clone)]
1527pub struct NewTrainingSession {
1528 #[serde(rename = "id")]
1529 pub task_id: TaskID,
1530 #[serde(rename = "train_session_id", default)]
1531 pub session_id: Option<TrainingSessionID>,
1532}
1533
1534impl Display for NewTrainingSession {
1535 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1536 match self.session_id {
1537 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1538 None => write!(f, "task {} (no session)", self.task_id),
1539 }
1540 }
1541}
1542
1543#[derive(Deserialize, Debug, Clone)]
1551pub struct Tag {
1552 pub id: u64,
1554 pub name: String,
1556 #[serde(default)]
1558 pub dataset_id: u64,
1559}
1560
1561#[derive(Deserialize, Clone, Debug)]
1562pub struct DatasetParams {
1563 dataset_id: DatasetID,
1564 annotation_set_id: AnnotationSetID,
1565 #[serde(rename = "train_group_name")]
1566 train_group: String,
1567 #[serde(rename = "val_group_name")]
1568 val_group: String,
1569}
1570
1571impl DatasetParams {
1572 pub fn dataset_id(&self) -> DatasetID {
1573 self.dataset_id
1574 }
1575
1576 pub fn annotation_set_id(&self) -> AnnotationSetID {
1577 self.annotation_set_id
1578 }
1579
1580 pub fn train_group(&self) -> &str {
1581 &self.train_group
1582 }
1583
1584 pub fn val_group(&self) -> &str {
1585 &self.val_group
1586 }
1587}
1588
1589#[derive(Serialize, Debug, Clone)]
1590pub struct TasksListParams {
1591 #[serde(skip_serializing_if = "Option::is_none")]
1592 pub continue_token: Option<String>,
1593 #[serde(skip_serializing_if = "Option::is_none")]
1594 pub types: Option<Vec<String>>,
1595 #[serde(rename = "manage_types", skip_serializing_if = "Option::is_none")]
1596 pub manager: Option<Vec<String>>,
1597 #[serde(skip_serializing_if = "Option::is_none")]
1598 pub status: Option<Vec<String>>,
1599}
1600
1601#[derive(Debug, Clone, Serialize, Deserialize)]
1607pub struct TaskDataList {
1608 pub server: String,
1609 #[serde(rename = "organization_uid")]
1610 pub organization_uid: String,
1611 #[serde(default)]
1612 pub traces: Vec<String>,
1613 #[serde(default)]
1614 pub data: std::collections::HashMap<String, Vec<String>>,
1615}
1616
1617#[derive(Debug, Clone, Serialize, Deserialize)]
1622pub struct Job {
1623 #[serde(default)]
1625 pub code: String,
1626 #[serde(default)]
1628 pub title: String,
1629 #[serde(default)]
1631 pub job_name: String,
1632 #[serde(default)]
1634 pub job_id: String,
1635 #[serde(default)]
1637 pub state: String,
1638 #[serde(default)]
1640 pub launch: Option<DateTime<Utc>>,
1641 pub task_id: i64,
1646}
1647
1648impl Job {
1649 pub fn task_id(&self) -> TaskID {
1655 TaskID::from(self.task_id.max(0) as u64)
1656 }
1657}
1658
1659#[derive(Deserialize, Debug, Clone)]
1660pub struct TasksListResult {
1661 pub tasks: Vec<Task>,
1662 pub continue_token: Option<String>,
1663}
1664
1665#[derive(Deserialize, Debug, Clone)]
1666pub struct Task {
1667 id: TaskID,
1668 name: String,
1669 #[serde(rename = "type")]
1670 workflow: String,
1671 status: String,
1672 #[serde(rename = "manage_type")]
1673 manager: Option<String>,
1674 #[serde(rename = "instance_type")]
1675 instance: String,
1676 #[serde(rename = "date")]
1677 created: DateTime<Utc>,
1678}
1679
1680impl Task {
1681 pub fn id(&self) -> TaskID {
1682 self.id
1683 }
1684
1685 pub fn name(&self) -> &str {
1686 &self.name
1687 }
1688
1689 pub fn workflow(&self) -> &str {
1690 &self.workflow
1691 }
1692
1693 pub fn status(&self) -> &str {
1694 &self.status
1695 }
1696
1697 pub fn manager(&self) -> Option<&str> {
1698 self.manager.as_deref()
1699 }
1700
1701 pub fn instance(&self) -> &str {
1702 &self.instance
1703 }
1704
1705 pub fn created(&self) -> &DateTime<Utc> {
1706 &self.created
1707 }
1708}
1709
1710impl Display for Task {
1711 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1712 write!(
1713 f,
1714 "{} [{:?} {}] {}",
1715 self.id,
1716 self.manager(),
1717 self.workflow(),
1718 self.name()
1719 )
1720 }
1721}
1722
1723#[derive(Deserialize, Debug, Clone)]
1724pub struct TaskInfo {
1725 id: TaskID,
1726 project_id: Option<ProjectID>,
1727 #[serde(rename = "task_description", alias = "description", default)]
1728 description: String,
1729 #[serde(rename = "type")]
1730 workflow: String,
1731 status: Option<String>,
1732 #[serde(default)]
1733 progress: TaskProgress,
1734 #[serde(
1735 rename = "created_date",
1736 alias = "created",
1737 default = "default_datetime_utc"
1738 )]
1739 created: DateTime<Utc>,
1740 #[serde(
1741 rename = "end_date",
1742 alias = "completed",
1743 default = "default_datetime_utc"
1744 )]
1745 completed: DateTime<Utc>,
1746}
1747
1748fn default_datetime_utc() -> DateTime<Utc> {
1749 DateTime::UNIX_EPOCH
1750}
1751
1752impl Display for TaskInfo {
1753 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1754 write!(f, "{} {}: {}", self.id, self.workflow(), self.description())
1755 }
1756}
1757
1758impl TaskInfo {
1759 pub fn id(&self) -> TaskID {
1760 self.id
1761 }
1762
1763 pub fn project_id(&self) -> Option<ProjectID> {
1764 self.project_id
1765 }
1766
1767 pub fn description(&self) -> &str {
1768 &self.description
1769 }
1770
1771 pub fn workflow(&self) -> &str {
1772 &self.workflow
1773 }
1774
1775 pub fn status(&self) -> &Option<String> {
1776 &self.status
1777 }
1778
1779 pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1780 let t = client.task_status(self.id(), status).await?;
1781 self.status = Some(t.status);
1782 Ok(())
1783 }
1784
1785 pub fn stages(&self) -> HashMap<String, Stage> {
1786 match &self.progress.stages {
1787 Some(stages) => stages.clone(),
1788 None => HashMap::new(),
1789 }
1790 }
1791
1792 pub async fn update_stage(
1793 &mut self,
1794 client: &Client,
1795 stage: &str,
1796 status: &str,
1797 message: &str,
1798 percentage: u8,
1799 ) -> Result<(), Error> {
1800 client
1801 .update_stage(self.id(), stage, status, message, percentage)
1802 .await?;
1803 let t = client.task_info(self.id()).await?;
1804 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1805 Ok(())
1806 }
1807
1808 pub async fn set_stages(
1809 &mut self,
1810 client: &Client,
1811 stages: &[(&str, &str)],
1812 ) -> Result<(), Error> {
1813 client.set_stages(self.id(), stages).await?;
1814 let t = client.task_info(self.id()).await?;
1815 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1816 Ok(())
1817 }
1818
1819 pub async fn data_list(&self, client: &client::Client) -> Result<TaskDataList, Error> {
1835 let req = client::TaskDataListRequest {
1836 task_id: self.id().value(),
1837 };
1838 match client.rpc("task.data.list".to_owned(), Some(&req)).await {
1839 Ok(r) => Ok(r),
1840 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1841 "task.data.list",
1842 code,
1843 msg,
1844 Some(self.id()),
1845 )),
1846 Err(e) => Err(e),
1847 }
1848 }
1849
1850 pub async fn upload_data(
1871 &self,
1872 client: &client::Client,
1873 path: &std::path::Path,
1874 folder: Option<&str>,
1875 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1876 ) -> Result<(), Error> {
1877 use futures::StreamExt;
1878 use std::sync::{
1879 Arc,
1880 atomic::{AtomicUsize, Ordering},
1881 };
1882 use tokio_util::io::ReaderStream;
1883
1884 let file_name = path
1885 .file_name()
1886 .and_then(|s| s.to_str())
1887 .ok_or_else(|| Error::InvalidParameters("path must have a UTF-8 filename".into()))?
1888 .to_owned();
1889
1890 let file = tokio::fs::File::open(path).await?;
1891 let total = file.metadata().await?.len() as usize;
1892 let sent = Arc::new(AtomicUsize::new(0));
1893
1894 let reader_stream = ReaderStream::new(file);
1895 let sent_clone = sent.clone();
1896 let progress_clone = progress.clone();
1897 let progress_stream = reader_stream.inspect(move |chunk_result| {
1898 if let Ok(chunk) = chunk_result {
1899 let current = sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1900 if let Some(tx) = &progress_clone {
1906 let _ = tx.try_send(Progress {
1907 current,
1908 total,
1909 status: None,
1910 });
1911 }
1912 }
1913 });
1914
1915 let body = reqwest::Body::wrap_stream(progress_stream);
1916 let file_part = Part::stream_with_length(body, total as u64).file_name(file_name);
1917
1918 let mut form = Form::new().text("task_id", self.id().value().to_string());
1919 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
1920 form = form.text("folder", folder.to_owned());
1921 }
1922 form = form.part("file", file_part);
1923
1924 let result = match client.post_multipart("task.data.upload", form).await {
1925 Ok(_) => Ok(()),
1926 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1927 "task.data.upload",
1928 code,
1929 msg,
1930 Some(self.id()),
1931 )),
1932 Err(e) => Err(e),
1933 };
1934
1935 if result.is_ok()
1939 && let Some(tx) = progress
1940 {
1941 let _ = tx
1942 .send(Progress {
1943 current: total,
1944 total,
1945 status: None,
1946 })
1947 .await;
1948 }
1949 result
1950 }
1951
1952 pub async fn download_data(
1981 &self,
1982 client: &client::Client,
1983 file: &str,
1984 folder: Option<&str>,
1985 output_path: &std::path::Path,
1986 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1987 ) -> Result<(), Error> {
1988 let folder = folder.unwrap_or("").to_owned();
1989 let req = client::TaskDataDownloadRequest {
1990 task_id: self.id().value(),
1991 folder,
1992 file: file.to_owned(),
1993 };
1994 match client
1995 .rpc_download("task.data.download", &req, output_path, progress)
1996 .await
1997 {
1998 Ok(()) => Ok(()),
1999 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2000 "task.data.download",
2001 code,
2002 msg,
2003 Some(self.id()),
2004 )),
2005 Err(e) => Err(e),
2006 }
2007 }
2008
2009 pub async fn add_chart(
2037 &self,
2038 client: &client::Client,
2039 group: &str,
2040 name: &str,
2041 data: Parameter,
2042 params: Option<Parameter>,
2043 ) -> Result<(), Error> {
2044 client::validate_chart_args(group, name)?;
2045 let req = client::TaskChartAddRequest {
2046 task_id: self.id().value(),
2047 group_name: group.to_owned(),
2048 chart_name: name.to_owned(),
2049 params,
2050 data,
2051 };
2052 let _resp: serde_json::Value =
2053 match client.rpc("task.chart.add".to_owned(), Some(&req)).await {
2054 Ok(r) => r,
2055 Err(Error::RpcError(code, msg)) => {
2056 return Err(client::map_rpc_error(
2057 "task.chart.add",
2058 code,
2059 msg,
2060 Some(self.id()),
2061 ));
2062 }
2063 Err(e) => return Err(e),
2064 };
2065 Ok(())
2066 }
2067
2068 pub async fn list_charts(
2085 &self,
2086 client: &client::Client,
2087 group: Option<&str>,
2088 ) -> Result<TaskDataList, Error> {
2089 let req = client::TaskChartListRequest {
2090 task_id: self.id().value(),
2091 group_name: group.unwrap_or("").to_owned(),
2092 };
2093 match client.rpc("task.chart.list".to_owned(), Some(&req)).await {
2094 Ok(r) => Ok(r),
2095 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2096 "task.chart.list",
2097 code,
2098 msg,
2099 Some(self.id()),
2100 )),
2101 Err(e) => Err(e),
2102 }
2103 }
2104
2105 pub async fn get_chart(
2124 &self,
2125 client: &client::Client,
2126 group: &str,
2127 name: &str,
2128 ) -> Result<Parameter, Error> {
2129 client::validate_chart_args(group, name)?;
2130 let req = client::TaskChartGetRequest {
2131 task_id: self.id().value(),
2132 group_name: group.to_owned(),
2133 chart_name: name.to_owned(),
2134 };
2135 match client.rpc("task.chart.get".to_owned(), Some(&req)).await {
2136 Ok(r) => Ok(r),
2137 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2138 "task.chart.get",
2139 code,
2140 msg,
2141 Some(self.id()),
2142 )),
2143 Err(e) => Err(e),
2144 }
2145 }
2146
2147 pub fn created(&self) -> &DateTime<Utc> {
2148 &self.created
2149 }
2150
2151 pub fn completed(&self) -> &DateTime<Utc> {
2152 &self.completed
2153 }
2154}
2155
2156#[derive(Deserialize, Debug, Default, Clone)]
2157pub struct TaskProgress {
2158 stages: Option<HashMap<String, Stage>>,
2159}
2160
2161#[derive(Serialize, Debug, Clone)]
2162pub struct TaskStatus {
2163 #[serde(rename = "docker_task_id")]
2164 pub task_id: TaskID,
2165 pub status: String,
2166}
2167
2168#[derive(Serialize, Deserialize, Debug, Clone)]
2169pub struct Stage {
2170 #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
2171 task_id: Option<TaskID>,
2172 stage: String,
2173 #[serde(skip_serializing_if = "Option::is_none")]
2174 status: Option<String>,
2175 #[serde(skip_serializing_if = "Option::is_none")]
2176 description: Option<String>,
2177 #[serde(skip_serializing_if = "Option::is_none")]
2178 message: Option<String>,
2179 percentage: u8,
2180}
2181
2182impl Stage {
2183 pub fn new(
2184 task_id: Option<TaskID>,
2185 stage: String,
2186 status: Option<String>,
2187 message: Option<String>,
2188 percentage: u8,
2189 ) -> Self {
2190 Stage {
2191 task_id,
2192 stage,
2193 status,
2194 description: None,
2195 message,
2196 percentage,
2197 }
2198 }
2199
2200 pub fn task_id(&self) -> &Option<TaskID> {
2201 &self.task_id
2202 }
2203
2204 pub fn stage(&self) -> &str {
2205 &self.stage
2206 }
2207
2208 pub fn status(&self) -> &Option<String> {
2209 &self.status
2210 }
2211
2212 pub fn description(&self) -> &Option<String> {
2213 &self.description
2214 }
2215
2216 pub fn message(&self) -> &Option<String> {
2217 &self.message
2218 }
2219
2220 pub fn percentage(&self) -> u8 {
2221 self.percentage
2222 }
2223}
2224
2225#[derive(Serialize, Debug)]
2226pub struct TaskStages {
2227 #[serde(rename = "docker_task_id")]
2228 pub task_id: TaskID,
2229 #[serde(skip_serializing_if = "Vec::is_empty")]
2230 pub stages: Vec<HashMap<String, String>>,
2231}
2232
2233#[derive(Deserialize, Debug)]
2234pub struct Artifact {
2235 name: String,
2236 #[serde(rename = "modelType")]
2237 model_type: String,
2238}
2239
2240impl Artifact {
2241 pub fn name(&self) -> &str {
2242 &self.name
2243 }
2244
2245 pub fn model_type(&self) -> &str {
2246 &self.model_type
2247 }
2248}
2249
2250#[derive(Deserialize, Serialize, Clone, Debug)]
2258pub struct VersionTag {
2259 id: u64,
2260 dataset_id: DatasetID,
2261 name: String,
2262 serial: u64,
2263 #[serde(default)]
2264 description: String,
2265 created_by: String,
2266 created_at: DateTime<Utc>,
2267 #[serde(default)]
2268 image_count: u64,
2269 #[serde(default)]
2270 annotation_counts: HashMap<String, u64>,
2271 #[serde(default)]
2272 sensor_counts: HashMap<String, u64>,
2273 #[serde(default)]
2274 label_count: u64,
2275 #[serde(default)]
2276 annotation_set_count: u64,
2277 #[serde(default)]
2278 snapshot_id: Option<u64>,
2279 #[serde(default)]
2280 is_current: bool,
2281}
2282
2283impl VersionTag {
2284 pub fn id(&self) -> u64 {
2286 self.id
2287 }
2288
2289 pub fn dataset_id(&self) -> DatasetID {
2291 self.dataset_id
2292 }
2293
2294 pub fn name(&self) -> &str {
2296 &self.name
2297 }
2298
2299 pub fn serial(&self) -> u64 {
2301 self.serial
2302 }
2303
2304 pub fn description(&self) -> &str {
2306 &self.description
2307 }
2308
2309 pub fn created_by(&self) -> &str {
2311 &self.created_by
2312 }
2313
2314 pub fn created_at(&self) -> DateTime<Utc> {
2316 self.created_at
2317 }
2318
2319 pub fn image_count(&self) -> u64 {
2321 self.image_count
2322 }
2323
2324 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2326 &self.annotation_counts
2327 }
2328
2329 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2331 &self.sensor_counts
2332 }
2333
2334 pub fn label_count(&self) -> u64 {
2336 self.label_count
2337 }
2338
2339 pub fn annotation_set_count(&self) -> u64 {
2341 self.annotation_set_count
2342 }
2343
2344 pub fn snapshot_id(&self) -> Option<u64> {
2346 self.snapshot_id
2347 }
2348
2349 pub fn is_current(&self) -> bool {
2352 self.is_current
2353 }
2354}
2355
2356impl Display for VersionTag {
2357 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2358 write!(f, "{} (serial {})", self.name, self.serial)
2359 }
2360}
2361
2362#[derive(Deserialize, Serialize, Clone, Debug)]
2364pub struct ChangelogEntry {
2365 id: u64,
2366 dataset_id: DatasetID,
2367 serial: u64,
2368 entity_type: String,
2369 operation: String,
2370 #[serde(default)]
2371 entity_id: Option<u64>,
2372 #[serde(default)]
2373 change_data: serde_json::Value,
2374 username: String,
2375 organization_id: u64,
2376 created_at: DateTime<Utc>,
2377 #[serde(default)]
2378 message: String,
2379 #[serde(default, deserialize_with = "deserialize_null_as_default")]
2380 s3_version_ids: Vec<serde_json::Value>,
2381}
2382
2383impl ChangelogEntry {
2384 pub fn id(&self) -> u64 {
2385 self.id
2386 }
2387
2388 pub fn dataset_id(&self) -> DatasetID {
2389 self.dataset_id
2390 }
2391
2392 pub fn serial(&self) -> u64 {
2394 self.serial
2395 }
2396
2397 pub fn entity_type(&self) -> &str {
2399 &self.entity_type
2400 }
2401
2402 pub fn operation(&self) -> &str {
2404 &self.operation
2405 }
2406
2407 pub fn entity_id(&self) -> Option<u64> {
2408 self.entity_id
2409 }
2410
2411 pub fn change_data(&self) -> &serde_json::Value {
2413 &self.change_data
2414 }
2415
2416 pub fn username(&self) -> &str {
2417 &self.username
2418 }
2419
2420 pub fn organization_id(&self) -> u64 {
2421 self.organization_id
2422 }
2423
2424 pub fn created_at(&self) -> DateTime<Utc> {
2425 self.created_at
2426 }
2427
2428 pub fn message(&self) -> &str {
2429 &self.message
2430 }
2431
2432 pub fn s3_version_ids(&self) -> &[serde_json::Value] {
2433 &self.s3_version_ids
2434 }
2435}
2436
2437#[derive(Deserialize, Debug, Clone)]
2439pub struct ChangelogResponse {
2440 pub entries: Vec<ChangelogEntry>,
2441 pub count: u64,
2442 #[serde(default)]
2443 pub continue_token: String,
2444 #[serde(default)]
2445 pub from_serial: Option<u64>,
2446 #[serde(default)]
2447 pub to_serial: Option<u64>,
2448}
2449
2450#[derive(Deserialize, Serialize, Clone, Debug)]
2452pub struct DatasetSummary {
2453 dataset_id: DatasetID,
2454 current_serial: u64,
2455 #[serde(default)]
2456 image_count: u64,
2457 #[serde(default)]
2458 annotation_counts: HashMap<String, u64>,
2459 #[serde(default)]
2460 sensor_counts: HashMap<String, u64>,
2461 #[serde(default)]
2462 label_count: u64,
2463 #[serde(default)]
2464 annotation_set_count: u64,
2465 last_updated: DateTime<Utc>,
2466}
2467
2468impl DatasetSummary {
2469 pub fn dataset_id(&self) -> DatasetID {
2470 self.dataset_id
2471 }
2472
2473 pub fn current_serial(&self) -> u64 {
2474 self.current_serial
2475 }
2476
2477 pub fn image_count(&self) -> u64 {
2478 self.image_count
2479 }
2480
2481 pub fn annotation_counts(&self) -> &HashMap<String, u64> {
2482 &self.annotation_counts
2483 }
2484
2485 pub fn sensor_counts(&self) -> &HashMap<String, u64> {
2486 &self.sensor_counts
2487 }
2488
2489 pub fn label_count(&self) -> u64 {
2490 self.label_count
2491 }
2492
2493 pub fn annotation_set_count(&self) -> u64 {
2494 self.annotation_set_count
2495 }
2496
2497 pub fn last_updated(&self) -> DateTime<Utc> {
2498 self.last_updated
2499 }
2500}
2501
2502#[derive(Deserialize, Debug, Clone)]
2504pub struct VersionCurrentResponse {
2505 pub dataset_id: DatasetID,
2506 pub current_serial: u64,
2507 #[serde(default)]
2508 pub latest_tag: Option<VersionTag>,
2509 #[serde(default)]
2510 pub tags: Vec<VersionTag>,
2511 #[serde(default)]
2512 pub summary: Option<DatasetSummary>,
2513}
2514
2515#[derive(Deserialize, Debug, Clone)]
2517pub struct RestoredFrom {
2518 pub tag: String,
2519 pub serial: u64,
2520}
2521
2522#[derive(Deserialize, Debug, Clone)]
2524pub struct RestoredCounts {
2525 pub images: u64,
2526 pub labels: u64,
2527 pub annotation_sets: u64,
2528}
2529
2530#[derive(Deserialize, Debug, Clone)]
2532pub struct RestoreResult {
2533 pub success: bool,
2534 pub new_serial: u64,
2535 pub restored_from: RestoredFrom,
2536 pub restored_counts: RestoredCounts,
2537 pub message: String,
2538}
2539
2540#[derive(Serialize)]
2543pub(crate) struct VersionTagCreateParams {
2544 pub dataset_id: DatasetID,
2545 pub name: String,
2546 #[serde(skip_serializing_if = "Option::is_none")]
2547 pub description: Option<String>,
2548}
2549
2550#[derive(Serialize)]
2551pub(crate) struct VersionTagNameParams {
2552 pub dataset_id: DatasetID,
2553 pub name: String,
2554}
2555
2556#[derive(Serialize)]
2557pub(crate) struct VersionChangelogParams {
2558 pub dataset_id: DatasetID,
2559 #[serde(skip_serializing_if = "Option::is_none")]
2560 pub from_version: Option<String>,
2561 #[serde(skip_serializing_if = "Option::is_none")]
2562 pub to_version: Option<String>,
2563 #[serde(skip_serializing_if = "Option::is_none")]
2564 pub entity_types: Option<Vec<String>>,
2565 #[serde(skip_serializing_if = "Option::is_none")]
2566 pub limit: Option<u64>,
2567 #[serde(skip_serializing_if = "Option::is_none")]
2568 pub continue_token: Option<String>,
2569}
2570
2571#[derive(Deserialize, Debug)]
2573pub(crate) struct ChangelogCountResult {
2574 pub count: u64,
2575}
2576
2577#[derive(Serialize, Deserialize, Debug, Clone)]
2584pub struct TrainerSchemaInfo {
2585 pub name: String,
2587 #[serde(default)]
2589 pub label: String,
2590 #[serde(default)]
2592 pub schema_type: String,
2593}
2594
2595#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
2601#[serde(rename_all = "lowercase")]
2602pub enum SchemaFieldType {
2603 Group,
2605 Slider,
2607 Select,
2609 Bool,
2611 Int,
2613 Float,
2615 Text,
2617 Date,
2619 Project,
2621 Dataset,
2623 Trainer,
2625 Upload,
2627 Info,
2630 #[serde(other)]
2632 Unknown,
2633}
2634
2635fn lenient_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
2639where
2640 D: Deserializer<'de>,
2641{
2642 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
2643 Ok(value.map(|v| match v {
2644 serde_json::Value::String(s) => s,
2645 other => other.to_string(),
2646 }))
2647}
2648
2649#[derive(Serialize, Deserialize, Debug, Clone)]
2651pub struct SchemaOption {
2652 #[serde(default)]
2654 pub name: Option<Parameter>,
2655 #[serde(default, deserialize_with = "lenient_string")]
2658 pub label: Option<String>,
2659 #[serde(default)]
2661 pub children: Vec<SchemaField>,
2662}
2663
2664#[derive(Serialize, Deserialize, Debug, Clone)]
2676pub struct SchemaField {
2677 #[serde(default, deserialize_with = "lenient_string")]
2679 pub name: Option<String>,
2680 #[serde(default, deserialize_with = "lenient_string")]
2682 pub label: Option<String>,
2683 #[serde(default, deserialize_with = "lenient_string")]
2685 pub description: Option<String>,
2686 #[serde(default)]
2688 pub required: bool,
2689 #[serde(default)]
2691 pub default: Option<Parameter>,
2692 #[serde(rename = "type", default)]
2694 pub field_type: Option<SchemaFieldType>,
2695 #[serde(default)]
2697 pub min: Option<f64>,
2698 #[serde(default)]
2700 pub max: Option<f64>,
2701 #[serde(default)]
2703 pub step: Option<f64>,
2704 #[serde(default)]
2706 pub options: Vec<SchemaOption>,
2707 #[serde(default)]
2710 pub children: Vec<SchemaField>,
2711 #[serde(default)]
2713 pub is_dropdown: bool,
2714 #[serde(default)]
2716 pub multi_select: bool,
2717 #[serde(default)]
2719 pub is_multi_line: bool,
2720 #[serde(default)]
2722 pub hidden: bool,
2723 #[serde(default)]
2725 pub numeric_only: bool,
2726 #[serde(default)]
2728 pub enable_tags_selection: bool,
2729 #[serde(default)]
2731 pub enable_annotation_set_selection: bool,
2732 #[serde(default)]
2734 pub values: Option<Vec<Parameter>>,
2735}
2736
2737#[derive(Serialize, Deserialize, Debug, Clone)]
2740pub struct ValidatorSchema {
2741 #[serde(rename = "type", default)]
2743 pub schema_type: String,
2744 #[serde(default)]
2746 pub name: String,
2747 #[serde(default)]
2749 pub schema: Vec<SchemaField>,
2750}
2751
2752#[cfg(test)]
2753mod tests {
2754 use super::*;
2755
2756 #[test]
2758 fn test_organization_id_from_u64() {
2759 let id = OrganizationID::from(12345);
2760 assert_eq!(id.value(), 12345);
2761 }
2762
2763 #[test]
2764 fn test_organization_id_display() {
2765 let id = OrganizationID::from(0xabc123);
2766 assert_eq!(format!("{}", id), "org-abc123");
2767 }
2768
2769 #[test]
2770 fn test_organization_id_try_from_str_valid() {
2771 let id = OrganizationID::try_from("org-abc123").unwrap();
2772 assert_eq!(id.value(), 0xabc123);
2773 }
2774
2775 #[test]
2776 fn test_organization_id_try_from_str_invalid_prefix() {
2777 let result = OrganizationID::try_from("invalid-abc123");
2778 assert!(result.is_err());
2779 match result {
2780 Err(Error::InvalidParameters(msg)) => {
2781 assert!(msg.contains("must start with 'org-'"));
2782 }
2783 _ => panic!("Expected InvalidParameters error"),
2784 }
2785 }
2786
2787 #[test]
2788 fn test_organization_id_try_from_str_invalid_hex() {
2789 let result = OrganizationID::try_from("org-xyz");
2790 assert!(result.is_err());
2791 }
2792
2793 #[test]
2794 fn test_organization_id_try_from_str_empty() {
2795 let result = OrganizationID::try_from("org-");
2796 assert!(result.is_err());
2797 }
2798
2799 #[test]
2800 fn test_organization_id_into_u64() {
2801 let id = OrganizationID::from(54321);
2802 let value: u64 = id.into();
2803 assert_eq!(value, 54321);
2804 }
2805
2806 #[test]
2808 fn test_usage_summary_deserialize_and_accessors() {
2809 let usage: UsageSummary = serde_json::from_str(
2810 r#"{"credits": 12.5, "funds": 49092.92, "total_funds_and_credits": 49105.42}"#,
2811 )
2812 .unwrap();
2813 assert_eq!(usage.credits(), 12.5);
2814 assert_eq!(usage.funds(), 49092.92);
2815 assert_eq!(usage.total(), 49105.42);
2816 }
2817
2818 #[test]
2819 fn test_usage_summary_defaults_for_missing_fields() {
2820 let usage: UsageSummary = serde_json::from_str("{}").unwrap();
2824 assert_eq!(usage.credits(), 0.0);
2825 assert_eq!(usage.funds(), 0.0);
2826 assert_eq!(usage.total(), 0.0);
2827 }
2828
2829 #[test]
2831 fn test_project_id_from_u64() {
2832 let id = ProjectID::from(78910);
2833 assert_eq!(id.value(), 78910);
2834 }
2835
2836 #[test]
2837 fn test_project_id_display() {
2838 let id = ProjectID::from(0xdef456);
2839 assert_eq!(format!("{}", id), "p-def456");
2840 }
2841
2842 #[test]
2843 fn test_project_id_from_str_valid() {
2844 let id = ProjectID::from_str("p-def456").unwrap();
2845 assert_eq!(id.value(), 0xdef456);
2846 }
2847
2848 #[test]
2849 fn test_project_id_try_from_str_valid() {
2850 let id = ProjectID::try_from("p-123abc").unwrap();
2851 assert_eq!(id.value(), 0x123abc);
2852 }
2853
2854 #[test]
2855 fn test_project_id_try_from_string_valid() {
2856 let id = ProjectID::try_from("p-456def".to_string()).unwrap();
2857 assert_eq!(id.value(), 0x456def);
2858 }
2859
2860 #[test]
2861 fn test_project_id_from_str_invalid_prefix() {
2862 let result = ProjectID::from_str("proj-123");
2863 assert!(result.is_err());
2864 match result {
2865 Err(Error::InvalidParameters(msg)) => {
2866 assert!(msg.contains("must start with 'p-'"));
2867 }
2868 _ => panic!("Expected InvalidParameters error"),
2869 }
2870 }
2871
2872 #[test]
2873 fn test_project_id_from_str_invalid_hex() {
2874 let result = ProjectID::from_str("p-notahex");
2875 assert!(result.is_err());
2876 }
2877
2878 #[test]
2879 fn test_project_id_into_u64() {
2880 let id = ProjectID::from(99999);
2881 let value: u64 = id.into();
2882 assert_eq!(value, 99999);
2883 }
2884
2885 #[test]
2887 fn test_experiment_id_from_u64() {
2888 let id = ExperimentID::from(1193046);
2889 assert_eq!(id.value(), 1193046);
2890 }
2891
2892 #[test]
2893 fn test_experiment_id_display() {
2894 let id = ExperimentID::from(0x123abc);
2895 assert_eq!(format!("{}", id), "exp-123abc");
2896 }
2897
2898 #[test]
2899 fn test_experiment_id_from_str_valid() {
2900 let id = ExperimentID::from_str("exp-456def").unwrap();
2901 assert_eq!(id.value(), 0x456def);
2902 }
2903
2904 #[test]
2905 fn test_experiment_id_try_from_str_valid() {
2906 let id = ExperimentID::try_from("exp-789abc").unwrap();
2907 assert_eq!(id.value(), 0x789abc);
2908 }
2909
2910 #[test]
2911 fn test_experiment_id_try_from_string_valid() {
2912 let id = ExperimentID::try_from("exp-fedcba".to_string()).unwrap();
2913 assert_eq!(id.value(), 0xfedcba);
2914 }
2915
2916 #[test]
2917 fn test_experiment_id_from_str_invalid_prefix() {
2918 let result = ExperimentID::from_str("experiment-123");
2919 assert!(result.is_err());
2920 match result {
2921 Err(Error::InvalidParameters(msg)) => {
2922 assert!(msg.contains("must start with 'exp-'"));
2923 }
2924 _ => panic!("Expected InvalidParameters error"),
2925 }
2926 }
2927
2928 #[test]
2929 fn test_experiment_id_from_str_invalid_hex() {
2930 let result = ExperimentID::from_str("exp-zzz");
2931 assert!(result.is_err());
2932 }
2933
2934 #[test]
2935 fn test_experiment_id_into_u64() {
2936 let id = ExperimentID::from(777777);
2937 let value: u64 = id.into();
2938 assert_eq!(value, 777777);
2939 }
2940
2941 #[test]
2943 fn test_training_session_id_from_u64() {
2944 let id = TrainingSessionID::from(7901234);
2945 assert_eq!(id.value(), 7901234);
2946 }
2947
2948 #[test]
2949 fn test_training_session_id_display() {
2950 let id = TrainingSessionID::from(0xabc123);
2951 assert_eq!(format!("{}", id), "t-abc123");
2952 }
2953
2954 #[test]
2955 fn test_training_session_id_from_str_valid() {
2956 let id = TrainingSessionID::from_str("t-abc123").unwrap();
2957 assert_eq!(id.value(), 0xabc123);
2958 }
2959
2960 #[test]
2961 fn test_training_session_id_try_from_str_valid() {
2962 let id = TrainingSessionID::try_from("t-deadbeef").unwrap();
2963 assert_eq!(id.value(), 0xdeadbeef);
2964 }
2965
2966 #[test]
2967 fn test_training_session_id_try_from_string_valid() {
2968 let id = TrainingSessionID::try_from("t-cafebabe".to_string()).unwrap();
2969 assert_eq!(id.value(), 0xcafebabe);
2970 }
2971
2972 #[test]
2973 fn test_training_session_id_from_str_invalid_prefix() {
2974 let result = TrainingSessionID::from_str("training-123");
2975 assert!(result.is_err());
2976 match result {
2977 Err(Error::InvalidParameters(msg)) => {
2978 assert!(msg.contains("must start with 't-'"));
2979 }
2980 _ => panic!("Expected InvalidParameters error"),
2981 }
2982 }
2983
2984 #[test]
2985 fn test_training_session_id_from_str_invalid_hex() {
2986 let result = TrainingSessionID::from_str("t-qqq");
2987 assert!(result.is_err());
2988 }
2989
2990 #[test]
2991 fn test_training_session_id_into_u64() {
2992 let id = TrainingSessionID::from(123456);
2993 let value: u64 = id.into();
2994 assert_eq!(value, 123456);
2995 }
2996
2997 #[test]
2999 fn test_validation_session_id_from_u64() {
3000 let id = ValidationSessionID::from(3456789);
3001 assert_eq!(id.value(), 3456789);
3002 }
3003
3004 #[test]
3005 fn test_validation_session_id_display() {
3006 let id = ValidationSessionID::from(0x34c985);
3007 assert_eq!(format!("{}", id), "v-34c985");
3008 }
3009
3010 #[test]
3011 fn test_validation_session_id_try_from_str_valid() {
3012 let id = ValidationSessionID::try_from("v-deadbeef").unwrap();
3013 assert_eq!(id.value(), 0xdeadbeef);
3014 }
3015
3016 #[test]
3017 fn test_validation_session_id_try_from_string_valid() {
3018 let id = ValidationSessionID::try_from("v-12345678".to_string()).unwrap();
3019 assert_eq!(id.value(), 0x12345678);
3020 }
3021
3022 #[test]
3023 fn test_validation_session_id_try_from_str_invalid_prefix() {
3024 let result = ValidationSessionID::try_from("validation-123");
3025 assert!(result.is_err());
3026 match result {
3027 Err(Error::InvalidParameters(msg)) => {
3028 assert!(msg.contains("must start with 'v-'"));
3029 }
3030 _ => panic!("Expected InvalidParameters error"),
3031 }
3032 }
3033
3034 #[test]
3035 fn test_validation_session_id_try_from_str_invalid_hex() {
3036 let result = ValidationSessionID::try_from("v-xyz");
3037 assert!(result.is_err());
3038 }
3039
3040 #[test]
3041 fn test_validation_session_id_into_u64() {
3042 let id = ValidationSessionID::from(987654);
3043 let value: u64 = id.into();
3044 assert_eq!(value, 987654);
3045 }
3046
3047 #[test]
3049 fn test_snapshot_id_from_u64() {
3050 let id = SnapshotID::from(111222);
3051 assert_eq!(id.value(), 111222);
3052 }
3053
3054 #[test]
3055 fn test_snapshot_id_display() {
3056 let id = SnapshotID::from(0xaabbcc);
3057 assert_eq!(format!("{}", id), "ss-aabbcc");
3058 }
3059
3060 #[test]
3061 fn test_snapshot_id_try_from_str_valid() {
3062 let id = SnapshotID::try_from("ss-aabbcc").unwrap();
3063 assert_eq!(id.value(), 0xaabbcc);
3064 }
3065
3066 #[test]
3067 fn test_snapshot_id_try_from_str_invalid_prefix() {
3068 let result = SnapshotID::try_from("snapshot-123");
3069 assert!(result.is_err());
3070 match result {
3071 Err(Error::InvalidParameters(msg)) => {
3072 assert!(msg.contains("must start with 'ss-'"));
3073 }
3074 _ => panic!("Expected InvalidParameters error"),
3075 }
3076 }
3077
3078 #[test]
3079 fn test_snapshot_id_try_from_str_invalid_hex() {
3080 let result = SnapshotID::try_from("ss-ggg");
3081 assert!(result.is_err());
3082 }
3083
3084 #[test]
3085 fn test_snapshot_id_into_u64() {
3086 let id = SnapshotID::from(333444);
3087 let value: u64 = id.into();
3088 assert_eq!(value, 333444);
3089 }
3090
3091 #[test]
3093 fn test_task_id_from_u64() {
3094 let id = TaskID::from(555666);
3095 assert_eq!(id.value(), 555666);
3096 }
3097
3098 #[test]
3099 fn test_task_id_display() {
3100 let id = TaskID::from(0x123456);
3101 assert_eq!(format!("{}", id), "task-123456");
3102 }
3103
3104 #[test]
3105 fn test_task_id_from_str_valid() {
3106 let id = TaskID::from_str("task-123456").unwrap();
3107 assert_eq!(id.value(), 0x123456);
3108 }
3109
3110 #[test]
3111 fn test_task_id_try_from_str_valid() {
3112 let id = TaskID::try_from("task-abcdef").unwrap();
3113 assert_eq!(id.value(), 0xabcdef);
3114 }
3115
3116 #[test]
3117 fn test_task_id_try_from_string_valid() {
3118 let id = TaskID::try_from("task-fedcba".to_string()).unwrap();
3119 assert_eq!(id.value(), 0xfedcba);
3120 }
3121
3122 #[test]
3123 fn test_task_id_from_str_invalid_prefix() {
3124 let result = TaskID::from_str("t-123");
3125 assert!(result.is_err());
3126 match result {
3127 Err(Error::InvalidParameters(msg)) => {
3128 assert!(msg.contains("must start with 'task-'"));
3129 }
3130 _ => panic!("Expected InvalidParameters error"),
3131 }
3132 }
3133
3134 #[test]
3135 fn test_task_id_from_str_invalid_hex() {
3136 let result = TaskID::from_str("task-zzz");
3137 assert!(result.is_err());
3138 }
3139
3140 #[test]
3141 fn test_task_id_into_u64() {
3142 let id = TaskID::from(777888);
3143 let value: u64 = id.into();
3144 assert_eq!(value, 777888);
3145 }
3146
3147 #[test]
3149 fn test_dataset_id_from_u64() {
3150 let id = DatasetID::from(1193046);
3151 assert_eq!(id.value(), 1193046);
3152 }
3153
3154 #[test]
3155 fn test_dataset_id_display() {
3156 let id = DatasetID::from(0x123abc);
3157 assert_eq!(format!("{}", id), "ds-123abc");
3158 }
3159
3160 #[test]
3161 fn test_dataset_id_from_str_valid() {
3162 let id = DatasetID::from_str("ds-456def").unwrap();
3163 assert_eq!(id.value(), 0x456def);
3164 }
3165
3166 #[test]
3167 fn test_dataset_id_try_from_str_valid() {
3168 let id = DatasetID::try_from("ds-789abc").unwrap();
3169 assert_eq!(id.value(), 0x789abc);
3170 }
3171
3172 #[test]
3173 fn test_dataset_id_try_from_string_valid() {
3174 let id = DatasetID::try_from("ds-fedcba".to_string()).unwrap();
3175 assert_eq!(id.value(), 0xfedcba);
3176 }
3177
3178 #[test]
3179 fn test_dataset_id_from_str_invalid_prefix() {
3180 let result = DatasetID::from_str("dataset-123");
3181 assert!(result.is_err());
3182 match result {
3183 Err(Error::InvalidParameters(msg)) => {
3184 assert!(msg.contains("must start with 'ds-'"));
3185 }
3186 _ => panic!("Expected InvalidParameters error"),
3187 }
3188 }
3189
3190 #[test]
3191 fn test_dataset_id_from_str_invalid_hex() {
3192 let result = DatasetID::from_str("ds-zzz");
3193 assert!(result.is_err());
3194 }
3195
3196 #[test]
3197 fn test_dataset_id_into_u64() {
3198 let id = DatasetID::from(111111);
3199 let value: u64 = id.into();
3200 assert_eq!(value, 111111);
3201 }
3202
3203 #[test]
3205 fn test_annotation_set_id_from_u64() {
3206 let id = AnnotationSetID::from(222333);
3207 assert_eq!(id.value(), 222333);
3208 }
3209
3210 #[test]
3211 fn test_annotation_set_id_display() {
3212 let id = AnnotationSetID::from(0xabcdef);
3213 assert_eq!(format!("{}", id), "as-abcdef");
3214 }
3215
3216 #[test]
3217 fn test_annotation_set_id_from_str_valid() {
3218 let id = AnnotationSetID::from_str("as-abcdef").unwrap();
3219 assert_eq!(id.value(), 0xabcdef);
3220 }
3221
3222 #[test]
3223 fn test_annotation_set_id_try_from_str_valid() {
3224 let id = AnnotationSetID::try_from("as-123456").unwrap();
3225 assert_eq!(id.value(), 0x123456);
3226 }
3227
3228 #[test]
3229 fn test_annotation_set_id_try_from_string_valid() {
3230 let id = AnnotationSetID::try_from("as-fedcba".to_string()).unwrap();
3231 assert_eq!(id.value(), 0xfedcba);
3232 }
3233
3234 #[test]
3235 fn test_annotation_set_id_from_str_invalid_prefix() {
3236 let result = AnnotationSetID::from_str("annotation-123");
3237 assert!(result.is_err());
3238 match result {
3239 Err(Error::InvalidParameters(msg)) => {
3240 assert!(msg.contains("must start with 'as-'"));
3241 }
3242 _ => panic!("Expected InvalidParameters error"),
3243 }
3244 }
3245
3246 #[test]
3247 fn test_annotation_set_id_from_str_invalid_hex() {
3248 let result = AnnotationSetID::from_str("as-zzz");
3249 assert!(result.is_err());
3250 }
3251
3252 #[test]
3253 fn test_annotation_set_id_into_u64() {
3254 let id = AnnotationSetID::from(444555);
3255 let value: u64 = id.into();
3256 assert_eq!(value, 444555);
3257 }
3258
3259 #[test]
3261 fn test_sample_id_from_u64() {
3262 let id = SampleID::from(666777);
3263 assert_eq!(id.value(), 666777);
3264 }
3265
3266 #[test]
3267 fn test_sample_id_display() {
3268 let id = SampleID::from(0x987654);
3269 assert_eq!(format!("{}", id), "s-987654");
3270 }
3271
3272 #[test]
3273 fn test_sample_id_try_from_str_valid() {
3274 let id = SampleID::try_from("s-987654").unwrap();
3275 assert_eq!(id.value(), 0x987654);
3276 }
3277
3278 #[test]
3279 fn test_sample_id_try_from_str_invalid_prefix() {
3280 let result = SampleID::try_from("sample-123");
3281 assert!(result.is_err());
3282 match result {
3283 Err(Error::InvalidParameters(msg)) => {
3284 assert!(msg.contains("must start with 's-'"));
3285 }
3286 _ => panic!("Expected InvalidParameters error"),
3287 }
3288 }
3289
3290 #[test]
3291 fn test_sample_id_try_from_str_invalid_hex() {
3292 let result = SampleID::try_from("s-zzz");
3293 assert!(result.is_err());
3294 }
3295
3296 #[test]
3297 fn test_sample_id_into_u64() {
3298 let id = SampleID::from(888999);
3299 let value: u64 = id.into();
3300 assert_eq!(value, 888999);
3301 }
3302
3303 #[test]
3305 fn test_app_id_from_u64() {
3306 let id = AppId::from(123123);
3307 assert_eq!(id.value(), 123123);
3308 }
3309
3310 #[test]
3311 fn test_app_id_display() {
3312 let id = AppId::from(0x456789);
3313 assert_eq!(format!("{}", id), "app-456789");
3314 }
3315
3316 #[test]
3317 fn test_app_id_try_from_str_valid() {
3318 let id = AppId::try_from("app-456789").unwrap();
3319 assert_eq!(id.value(), 0x456789);
3320 }
3321
3322 #[test]
3323 fn test_app_id_try_from_str_invalid_prefix() {
3324 let result = AppId::try_from("application-123");
3325 assert!(result.is_err());
3326 match result {
3327 Err(Error::InvalidParameters(msg)) => {
3328 assert!(msg.contains("must start with 'app-'"));
3329 }
3330 _ => panic!("Expected InvalidParameters error"),
3331 }
3332 }
3333
3334 #[test]
3335 fn test_app_id_try_from_str_invalid_hex() {
3336 let result = AppId::try_from("app-zzz");
3337 assert!(result.is_err());
3338 }
3339
3340 #[test]
3341 fn test_app_id_into_u64() {
3342 let id = AppId::from(321321);
3343 let value: u64 = id.into();
3344 assert_eq!(value, 321321);
3345 }
3346
3347 #[test]
3349 fn test_image_id_from_u64() {
3350 let id = ImageId::from(789789);
3351 assert_eq!(id.value(), 789789);
3352 }
3353
3354 #[test]
3355 fn test_image_id_display() {
3356 let id = ImageId::from(0xabcd1234);
3357 assert_eq!(format!("{}", id), "im-abcd1234");
3358 }
3359
3360 #[test]
3361 fn test_image_id_try_from_str_valid() {
3362 let id = ImageId::try_from("im-abcd1234").unwrap();
3363 assert_eq!(id.value(), 0xabcd1234);
3364 }
3365
3366 #[test]
3367 fn test_image_id_try_from_str_invalid_prefix() {
3368 let result = ImageId::try_from("image-123");
3369 assert!(result.is_err());
3370 match result {
3371 Err(Error::InvalidParameters(msg)) => {
3372 assert!(msg.contains("must start with 'im-'"));
3373 }
3374 _ => panic!("Expected InvalidParameters error"),
3375 }
3376 }
3377
3378 #[test]
3379 fn test_image_id_try_from_str_invalid_hex() {
3380 let result = ImageId::try_from("im-zzz");
3381 assert!(result.is_err());
3382 }
3383
3384 #[test]
3385 fn test_image_id_into_u64() {
3386 let id = ImageId::from(987987);
3387 let value: u64 = id.into();
3388 assert_eq!(value, 987987);
3389 }
3390
3391 #[test]
3393 fn test_id_types_equality() {
3394 let id1 = ProjectID::from(12345);
3395 let id2 = ProjectID::from(12345);
3396 let id3 = ProjectID::from(54321);
3397
3398 assert_eq!(id1, id2);
3399 assert_ne!(id1, id3);
3400 }
3401
3402 #[test]
3403 fn test_id_types_hash() {
3404 use std::collections::HashSet;
3405
3406 let mut set = HashSet::new();
3407 set.insert(DatasetID::from(100));
3408 set.insert(DatasetID::from(200));
3409 set.insert(DatasetID::from(100)); assert_eq!(set.len(), 2);
3412 assert!(set.contains(&DatasetID::from(100)));
3413 assert!(set.contains(&DatasetID::from(200)));
3414 }
3415
3416 #[test]
3417 fn test_id_types_copy_clone() {
3418 let id1 = ExperimentID::from(999);
3419 let id2 = id1; let id3 = id1; assert_eq!(id1, id2);
3423 assert_eq!(id1, id3);
3424 }
3425
3426 #[test]
3428 fn test_id_zero_value() {
3429 let id = ProjectID::from(0);
3430 assert_eq!(format!("{}", id), "p-0");
3431 assert_eq!(id.value(), 0);
3432 }
3433
3434 #[test]
3435 fn test_id_max_value() {
3436 let id = ProjectID::from(u64::MAX);
3437 assert_eq!(format!("{}", id), "p-ffffffffffffffff");
3438 assert_eq!(id.value(), u64::MAX);
3439 }
3440
3441 #[test]
3442 fn test_id_round_trip_conversion() {
3443 let original = 0xdeadbeef_u64;
3444 let id = TrainingSessionID::from(original);
3445 let back: u64 = id.into();
3446 assert_eq!(original, back);
3447 }
3448
3449 #[test]
3450 fn test_id_case_insensitive_hex() {
3451 let id1 = DatasetID::from_str("ds-ABCDEF").unwrap();
3453 let id2 = DatasetID::from_str("ds-abcdef").unwrap();
3454 assert_eq!(id1.value(), id2.value());
3455 }
3456
3457 #[test]
3458 fn test_id_with_leading_zeros() {
3459 let id = ProjectID::from_str("p-00001234").unwrap();
3460 assert_eq!(id.value(), 0x1234);
3461 }
3462
3463 #[test]
3465 fn test_parameter_integer() {
3466 let param = Parameter::Integer(42);
3467 match param {
3468 Parameter::Integer(val) => assert_eq!(val, 42),
3469 _ => panic!("Expected Integer variant"),
3470 }
3471 }
3472
3473 #[test]
3474 fn test_parameter_real() {
3475 let param = Parameter::Real(2.5);
3476 match param {
3477 Parameter::Real(val) => assert_eq!(val, 2.5),
3478 _ => panic!("Expected Real variant"),
3479 }
3480 }
3481
3482 #[test]
3483 fn test_parameter_boolean() {
3484 let param = Parameter::Boolean(true);
3485 match param {
3486 Parameter::Boolean(val) => assert!(val),
3487 _ => panic!("Expected Boolean variant"),
3488 }
3489 }
3490
3491 #[test]
3492 fn test_parameter_string() {
3493 let param = Parameter::String("test".to_string());
3494 match param {
3495 Parameter::String(val) => assert_eq!(val, "test"),
3496 _ => panic!("Expected String variant"),
3497 }
3498 }
3499
3500 #[test]
3501 fn test_parameter_array() {
3502 let param = Parameter::Array(vec![
3503 Parameter::Integer(1),
3504 Parameter::Integer(2),
3505 Parameter::Integer(3),
3506 ]);
3507 match param {
3508 Parameter::Array(arr) => assert_eq!(arr.len(), 3),
3509 _ => panic!("Expected Array variant"),
3510 }
3511 }
3512
3513 #[test]
3514 fn test_parameter_object() {
3515 let mut map = HashMap::new();
3516 map.insert("key".to_string(), Parameter::Integer(100));
3517 let param = Parameter::Object(map);
3518 match param {
3519 Parameter::Object(obj) => {
3520 assert_eq!(obj.len(), 1);
3521 assert!(obj.contains_key("key"));
3522 }
3523 _ => panic!("Expected Object variant"),
3524 }
3525 }
3526
3527 #[test]
3528 fn test_parameter_clone() {
3529 let param1 = Parameter::Integer(42);
3530 let param2 = param1.clone();
3531 assert_eq!(param1, param2);
3532 }
3533
3534 #[test]
3535 fn test_parameter_nested() {
3536 let inner_array = Parameter::Array(vec![Parameter::Integer(1), Parameter::Integer(2)]);
3537 let outer_array = Parameter::Array(vec![inner_array.clone(), inner_array]);
3538
3539 match outer_array {
3540 Parameter::Array(arr) => {
3541 assert_eq!(arr.len(), 2);
3542 }
3543 _ => panic!("Expected Array variant"),
3544 }
3545 }
3546
3547 macro_rules! test_typeid_conversions {
3550 ($test_name:ident, $type:ty, $prefix:literal, $wrong_prefix:literal) => {
3551 #[test]
3552 fn $test_name() {
3553 let id = <$type>::from(0xabc123);
3555 assert_eq!(id.value(), 0xabc123);
3556
3557 assert_eq!(format!("{}", id), concat!($prefix, "-abc123"));
3559
3560 let id: $type = concat!($prefix, "-abc123").parse().unwrap();
3562 assert_eq!(id.value(), 0xabc123);
3563
3564 assert!(concat!($wrong_prefix, "-abc").parse::<$type>().is_err());
3566
3567 assert!("abc123".parse::<$type>().is_err());
3569
3570 assert!(concat!($prefix, "-xyz").parse::<$type>().is_err());
3572
3573 let id = <$type>::try_from(concat!($prefix, "-abc123")).unwrap();
3575 assert_eq!(id.value(), 0xabc123);
3576
3577 let id = <$type>::try_from(concat!($prefix, "-abc123").to_string()).unwrap();
3579 assert_eq!(id.value(), 0xabc123);
3580
3581 let id = <$type>::from(0xabc123);
3583 let json = serde_json::to_string(&id).unwrap();
3584 let parsed: $type = serde_json::from_str(&json).unwrap();
3585 assert_eq!(id, parsed);
3586
3587 let id = <$type>::from(0xabc123);
3589 let val: u64 = id.into();
3590 assert_eq!(val, 0xabc123);
3591 }
3592 };
3593 }
3594
3595 test_typeid_conversions!(test_organization_id_conversions, OrganizationID, "org", "p");
3596 test_typeid_conversions!(test_project_id_conversions, ProjectID, "p", "org");
3597 test_typeid_conversions!(test_experiment_id_conversions, ExperimentID, "exp", "p");
3598 test_typeid_conversions!(
3599 test_training_session_id_conversions,
3600 TrainingSessionID,
3601 "t",
3602 "v"
3603 );
3604 test_typeid_conversions!(
3605 test_validation_session_id_conversions,
3606 ValidationSessionID,
3607 "v",
3608 "t"
3609 );
3610 test_typeid_conversions!(test_snapshot_id_conversions, SnapshotID, "ss", "ds");
3611 test_typeid_conversions!(test_task_id_conversions, TaskID, "task", "t");
3612 test_typeid_conversions!(test_dataset_id_conversions, DatasetID, "ds", "ss");
3613 test_typeid_conversions!(
3614 test_annotation_set_id_conversions,
3615 AnnotationSetID,
3616 "as",
3617 "ds"
3618 );
3619 test_typeid_conversions!(test_sample_id_conversions, SampleID, "s", "p");
3620 test_typeid_conversions!(test_app_id_conversions, AppId, "app", "p");
3621 test_typeid_conversions!(test_image_id_conversions, ImageId, "im", "se");
3622 test_typeid_conversions!(test_sequence_id_conversions, SequenceId, "se", "im");
3623
3624 #[test]
3627 fn test_version_tag_deserialize_full() {
3628 let json = r#"{
3629 "id": 456, "dataset_id": 1715004, "name": "training-v1.0",
3630 "serial": 42, "description": "Ready for production",
3631 "created_by": "user@example.com", "created_at": "2025-01-15T10:30:00Z",
3632 "image_count": 50000, "annotation_counts": {"box": 150000, "seg": 20000},
3633 "sensor_counts": {"lidar": 25000}, "label_count": 15,
3634 "annotation_set_count": 3, "snapshot_id": 789
3635 }"#;
3636 let tag: VersionTag = serde_json::from_str(json).unwrap();
3637 assert_eq!(tag.name(), "training-v1.0");
3638 assert_eq!(tag.serial(), 42);
3639 assert_eq!(tag.image_count(), 50000);
3640 assert_eq!(tag.annotation_counts().get("box"), Some(&150000));
3641 assert_eq!(tag.snapshot_id(), Some(789));
3642 }
3643
3644 #[test]
3645 fn test_version_tag_deserialize_omitempty() {
3646 let json = r#"{
3648 "id": 1, "dataset_id": 2, "name": "v1.0", "serial": 5,
3649 "description": "", "created_by": "user",
3650 "created_at": "2025-01-01T00:00:00Z"
3651 }"#;
3652 let tag: VersionTag = serde_json::from_str(json).unwrap();
3653 assert_eq!(tag.snapshot_id(), None);
3654 assert_eq!(tag.image_count(), 0);
3655 assert!(tag.annotation_counts().is_empty());
3656 }
3657
3658 #[test]
3659 fn test_changelog_entry_deserialize_omitempty() {
3660 let json = r#"{
3662 "id": 1, "dataset_id": 2, "serial": 3, "entity_type": "image",
3663 "operation": "bulk_create", "change_data": {"count": 5},
3664 "username": "user", "organization_id": 1,
3665 "created_at": "2025-01-01T00:00:00Z", "message": ""
3666 }"#;
3667 let entry: ChangelogEntry = serde_json::from_str(json).unwrap();
3668 assert!(entry.entity_id().is_none());
3669 assert!(entry.s3_version_ids().is_empty());
3670 assert_eq!(entry.entity_type(), "image");
3671 assert_eq!(entry.operation(), "bulk_create");
3672 }
3673
3674 #[test]
3675 fn test_changelog_response_deserialize() {
3676 let json = r#"{
3677 "entries": [], "count": 0, "continue_token": ""
3678 }"#;
3679 let resp: ChangelogResponse = serde_json::from_str(json).unwrap();
3680 assert!(resp.entries.is_empty());
3681 assert_eq!(resp.count, 0);
3682 assert!(resp.continue_token.is_empty());
3683 assert!(resp.from_serial.is_none());
3684 }
3685
3686 #[test]
3687 fn test_version_current_no_latest_tag() {
3688 let json = r#"{
3690 "dataset_id": 100, "current_serial": 5, "tags": []
3691 }"#;
3692 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3693 assert!(resp.latest_tag.is_none());
3694 assert!(resp.tags.is_empty());
3695 assert_eq!(resp.current_serial, 5);
3696 }
3697
3698 #[test]
3699 fn test_version_current_with_latest_tag() {
3700 let json = r#"{
3701 "dataset_id": 100, "current_serial": 42,
3702 "latest_tag": {
3703 "id": 1, "dataset_id": 100, "name": "v1.0", "serial": 42,
3704 "description": "test", "created_by": "user",
3705 "created_at": "2025-01-01T00:00:00Z",
3706 "image_count": 10, "label_count": 2, "annotation_set_count": 1
3707 },
3708 "tags": []
3709 }"#;
3710 let resp: VersionCurrentResponse = serde_json::from_str(json).unwrap();
3711 assert!(resp.latest_tag.is_some());
3712 assert_eq!(resp.latest_tag.unwrap().name(), "v1.0");
3713 }
3714
3715 #[test]
3716 fn test_version_tag_is_current_field() {
3717 let json = r#"{
3718 "id": 1, "dataset_id": 5, "name": "v1.0", "serial": 10,
3719 "created_by": "alice", "created_at": "2026-01-01T00:00:00Z",
3720 "is_current": true
3721 }"#;
3722 let tag: VersionTag = serde_json::from_str(json).unwrap();
3723 assert!(tag.is_current());
3724 }
3725
3726 #[test]
3727 fn test_version_tag_is_current_false() {
3728 let json = r#"{
3729 "id": 1, "dataset_id": 5, "name": "v1.0", "serial": 10,
3730 "created_by": "alice", "created_at": "2026-01-01T00:00:00Z",
3731 "is_current": false
3732 }"#;
3733 let tag: VersionTag = serde_json::from_str(json).unwrap();
3734 assert!(!tag.is_current());
3735 }
3736
3737 #[test]
3738 fn test_dataset_summary_deserialize() {
3739 let json = r#"{
3740 "dataset_id": 100, "current_serial": 10,
3741 "image_count": 5000, "annotation_counts": {"box": 10000},
3742 "sensor_counts": {}, "label_count": 8,
3743 "annotation_set_count": 2, "last_updated": "2025-06-01T12:00:00Z"
3744 }"#;
3745 let summary: DatasetSummary = serde_json::from_str(json).unwrap();
3746 assert_eq!(summary.image_count(), 5000);
3747 assert_eq!(summary.label_count(), 8);
3748 assert_eq!(summary.annotation_counts().get("box"), Some(&10000));
3749 }
3750
3751 #[test]
3752 fn test_restore_result_deserialize() {
3753 let json = r#"{
3754 "success": true, "new_serial": 45,
3755 "restored_from": {"tag": "v1.0", "serial": 42},
3756 "restored_counts": {"images": 5000, "labels": 15, "annotation_sets": 3},
3757 "message": "Dataset restored to tag v1.0"
3758 }"#;
3759 let result: RestoreResult = serde_json::from_str(json).unwrap();
3760 assert!(result.success);
3761 assert_eq!(result.new_serial, 45);
3762 assert_eq!(result.restored_from.tag, "v1.0");
3763 assert_eq!(result.restored_from.serial, 42);
3764 assert_eq!(result.restored_counts.images, 5000);
3765 }
3766
3767 #[test]
3768 fn test_sample_delete_params_serializes_all_fields() {
3769 let params = SampleDeleteParams {
3775 dataset_id: 42,
3776 image_ids: vec![1, 2, 3],
3777 sequence_ids: Vec::new(),
3778 delete_all: false,
3779 };
3780 let value = serde_json::to_value(¶ms).unwrap();
3781 let obj = value.as_object().unwrap();
3782 assert_eq!(obj.len(), 4);
3783 assert_eq!(obj["dataset_id"], serde_json::json!(42));
3784 assert_eq!(obj["image_ids"], serde_json::json!([1, 2, 3]));
3785 assert_eq!(obj["sequence_ids"], serde_json::json!([]));
3786 assert_eq!(obj["delete_all"], serde_json::json!(false));
3787 }
3788}
3789
3790#[cfg(test)]
3791mod tests_task_data_list {
3792 use super::*;
3793
3794 #[test]
3795 fn task_data_list_deserializes_from_server_shape() {
3796 let json = r#"{
3797 "server": "test.edgefirst.studio",
3798 "organization_uid": "org-abc123",
3799 "traces": ["trace/imx95.json"],
3800 "data": {
3801 "predictions": ["predictions.parquet"],
3802 "trace": ["imx95.json"]
3803 }
3804 }"#;
3805 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
3806 assert_eq!(parsed.server, "test.edgefirst.studio");
3807 assert_eq!(parsed.organization_uid, "org-abc123");
3808 assert_eq!(parsed.traces, vec!["trace/imx95.json"]);
3809 assert_eq!(
3810 parsed.data.get("predictions").unwrap(),
3811 &vec!["predictions.parquet".to_string()]
3812 );
3813 }
3814}
3815
3816#[cfg(test)]
3817mod tests_upload_data {
3818 #[test]
3822 fn folder_empty_string_is_normalised() {
3823 let folder: Option<&str> = Some("");
3824 assert!(folder.filter(|s| !s.is_empty()).is_none());
3825
3826 let folder_real: Option<&str> = Some("predictions");
3827 assert!(folder_real.filter(|s| !s.is_empty()).is_some());
3828 }
3829}
3830
3831#[cfg(test)]
3832mod tests_job_struct {
3833 use super::*;
3834
3835 #[test]
3836 fn job_deserializes_with_all_fields() {
3837 let json = r#"{
3838 "code": "edgefirst-validator:2.9.5",
3839 "title": "EdgeFirst Validator",
3840 "job_name": "smoke-test",
3841 "job_id": "aws-batch-abc",
3842 "state": "RUNNING",
3843 "launch": "2026-05-14T15:00:00Z",
3844 "task_id": 6789
3845 }"#;
3846 let job: Job = serde_json::from_str(json).unwrap();
3847 assert_eq!(job.code, "edgefirst-validator:2.9.5");
3848 assert_eq!(job.title, "EdgeFirst Validator");
3849 assert_eq!(job.job_name, "smoke-test");
3850 assert_eq!(job.job_id, "aws-batch-abc");
3851 assert_eq!(job.state, "RUNNING");
3852 assert!(job.launch.is_some());
3853 assert_eq!(job.task_id, 6789);
3854 }
3855
3856 #[test]
3857 fn job_tolerates_missing_optional_fields() {
3858 let json = r#"{ "task_id": 42 }"#;
3862 let job: Job = serde_json::from_str(json).unwrap();
3863 assert_eq!(job.task_id, 42);
3864 assert!(job.code.is_empty());
3865 assert!(job.title.is_empty());
3866 assert!(job.job_name.is_empty());
3867 assert!(job.job_id.is_empty());
3868 assert!(job.state.is_empty());
3869 assert!(job.launch.is_none());
3870 }
3871
3872 #[test]
3873 fn job_task_id_accessor_saturates_negative_to_zero() {
3874 let job = Job {
3879 code: String::new(),
3880 title: String::new(),
3881 job_name: String::new(),
3882 job_id: String::new(),
3883 state: String::new(),
3884 launch: None,
3885 task_id: -1,
3886 };
3887 assert_eq!(job.task_id().value(), 0);
3888 }
3889
3890 #[test]
3891 fn job_task_id_accessor_passes_through_positive_values() {
3892 let job = Job {
3893 code: String::new(),
3894 title: String::new(),
3895 job_name: String::new(),
3896 job_id: String::new(),
3897 state: String::new(),
3898 launch: None,
3899 task_id: 12345,
3900 };
3901 assert_eq!(job.task_id().value(), 12345);
3902 }
3903
3904 #[test]
3905 fn job_ignores_unknown_fields() {
3906 let json = r#"{
3910 "code": "x",
3911 "task_id": 1,
3912 "docker_task": { "image": "x" },
3913 "aws_region": "us-east-1",
3914 "tags": ["a", "b"]
3915 }"#;
3916 let job: Job = serde_json::from_str(json).unwrap();
3917 assert_eq!(job.task_id, 1);
3918 }
3919}
3920
3921#[cfg(test)]
3922mod tests_task_info_schema_tolerance {
3923 use super::*;
3924
3925 #[test]
3930 fn task_info_accepts_task_description_field() {
3931 let json = r#"{
3933 "id": 6699,
3934 "type": "edgefirst-validator:2.9.5",
3935 "task_description": "Profiler run for IMX95",
3936 "status": "running"
3937 }"#;
3938 let info: TaskInfo = serde_json::from_str(json).unwrap();
3939 assert_eq!(info.description(), "Profiler run for IMX95");
3940 }
3941
3942 #[test]
3943 fn task_info_accepts_legacy_description_field() {
3944 let json = r#"{
3946 "id": 6699,
3947 "type": "edgefirst-validator:2.9.5",
3948 "description": "Legacy description"
3949 }"#;
3950 let info: TaskInfo = serde_json::from_str(json).unwrap();
3951 assert_eq!(info.description(), "Legacy description");
3952 }
3953
3954 #[test]
3955 fn task_info_tolerates_missing_description() {
3956 let json = r#"{
3958 "id": 6699,
3959 "type": "x"
3960 }"#;
3961 let info: TaskInfo = serde_json::from_str(json).unwrap();
3962 assert!(info.description().is_empty());
3963 }
3964
3965 #[test]
3966 fn task_info_tolerates_missing_dates_via_default() {
3967 let json = r#"{
3969 "id": 6699,
3970 "type": "x"
3971 }"#;
3972 let info: TaskInfo = serde_json::from_str(json).unwrap();
3973 assert_eq!(info.id().value(), 6699);
3975 }
3976
3977 #[test]
3978 fn task_info_status_accessor_returns_option() {
3979 let json = r#"{
3980 "id": 1,
3981 "type": "x"
3982 }"#;
3983 let info: TaskInfo = serde_json::from_str(json).unwrap();
3984 assert!(info.status().is_none());
3985 }
3986
3987 #[test]
3988 fn task_info_stages_returns_empty_map_when_unset() {
3989 let json = r#"{
3990 "id": 1,
3991 "type": "x"
3992 }"#;
3993 let info: TaskInfo = serde_json::from_str(json).unwrap();
3994 let stages = info.stages();
3995 assert!(stages.is_empty());
3996 }
3997}
3998
3999#[cfg(test)]
4000mod tests_stage_struct {
4001 use super::*;
4002
4003 #[test]
4004 fn stage_new_sets_only_supplied_fields() {
4005 let stage = Stage::new(
4006 None,
4007 "download".into(),
4008 Some("running".into()),
4009 Some("fetching".into()),
4010 42,
4011 );
4012 assert!(stage.task_id().is_none());
4013 assert_eq!(stage.stage(), "download");
4014 assert_eq!(stage.status().as_deref(), Some("running"));
4015 assert_eq!(stage.message().as_deref(), Some("fetching"));
4016 assert_eq!(stage.percentage(), 42);
4017 assert!(stage.description().is_none());
4019 }
4020
4021 #[test]
4022 fn stage_serializes_without_optional_none_fields() {
4023 let stage = Stage::new(None, "init".into(), None, None, 0);
4025 let json = serde_json::to_value(&stage).unwrap();
4026 assert!(json.get("status").is_none(), "got: {json}");
4027 assert!(json.get("message").is_none(), "got: {json}");
4028 assert!(json.get("docker_task_id").is_none(), "got: {json}");
4029 assert_eq!(json["stage"], "init");
4031 assert_eq!(json["percentage"], 0);
4032 }
4033
4034 #[test]
4035 fn stage_serializes_task_id_when_present() {
4036 let task_id = TaskID::from(0xdeadu64);
4037 let stage = Stage::new(Some(task_id), "x".into(), None, None, 0);
4038 let json = serde_json::to_value(&stage).unwrap();
4039 assert!(json.get("docker_task_id").is_some());
4042 }
4043
4044 #[test]
4045 fn stage_round_trips_through_json() {
4046 let stage = Stage::new(
4047 None,
4048 "train".into(),
4049 Some("done".into()),
4050 Some("epoch 100".into()),
4051 100,
4052 );
4053 let s = serde_json::to_string(&stage).unwrap();
4054 let back: Stage = serde_json::from_str(&s).unwrap();
4055 assert_eq!(back.stage(), "train");
4056 assert_eq!(back.status().as_deref(), Some("done"));
4057 assert_eq!(back.message().as_deref(), Some("epoch 100"));
4058 assert_eq!(back.percentage(), 100);
4059 }
4060}
4061
4062#[cfg(test)]
4063mod tests_task_data_list_extra {
4064 use super::*;
4065
4066 #[test]
4067 fn task_data_list_with_empty_data_map() {
4068 let json = r#"{
4069 "server": "studio",
4070 "organization_uid": "org-1",
4071 "traces": [],
4072 "data": {}
4073 }"#;
4074 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4075 assert!(parsed.traces.is_empty());
4076 assert!(parsed.data.is_empty());
4077 }
4078
4079 #[test]
4080 fn task_data_list_multiple_folders() {
4081 let json = r#"{
4082 "server": "studio",
4083 "organization_uid": "org-1",
4084 "traces": ["t1", "t2"],
4085 "data": {
4086 "predictions": ["a.parquet", "b.parquet"],
4087 "metrics": ["loss.json"]
4088 }
4089 }"#;
4090 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
4091 assert_eq!(parsed.traces.len(), 2);
4092 assert_eq!(parsed.data.len(), 2);
4093 assert_eq!(parsed.data["predictions"].len(), 2);
4094 }
4095}
4096
4097#[cfg(test)]
4098mod tests_artifact_struct {
4099 use super::*;
4100
4101 #[test]
4102 fn artifact_accessors_return_strs() {
4103 let json = r#"{ "name": "best.onnx", "modelType": "yolo" }"#;
4106 let a: Artifact = serde_json::from_str(json).unwrap();
4107 assert_eq!(a.name(), "best.onnx");
4108 assert_eq!(a.model_type(), "yolo");
4109 }
4110}
4111
4112#[cfg(test)]
4113mod tests_task_status_serialize {
4114 use super::*;
4115
4116 #[test]
4117 fn task_status_uses_docker_task_id_wire_field() {
4118 let s = TaskStatus {
4119 task_id: TaskID::from(0x1a2bu64),
4120 status: "training".into(),
4121 };
4122 let json = serde_json::to_value(&s).unwrap();
4123 assert!(json.get("docker_task_id").is_some(), "got: {json}");
4125 assert_eq!(json["status"], "training");
4126 }
4127}
4128
4129#[cfg(test)]
4130mod tests_task_stages_serialize {
4131 use super::*;
4132
4133 #[test]
4134 fn task_stages_omits_empty_vec() {
4135 let stages = TaskStages {
4136 task_id: TaskID::from(1u64),
4137 stages: Vec::new(),
4138 };
4139 let json = serde_json::to_value(&stages).unwrap();
4140 assert!(json.get("stages").is_none(), "got: {json}");
4142 }
4143
4144 #[test]
4145 fn task_stages_serializes_non_empty_vec() {
4146 let stages = TaskStages {
4147 task_id: TaskID::from(1u64),
4148 stages: vec![std::collections::HashMap::from([(
4149 "stage".to_string(),
4150 "download".to_string(),
4151 )])],
4152 };
4153 let json = serde_json::to_value(&stages).unwrap();
4154 assert_eq!(json["stages"][0]["stage"], "download");
4155 }
4156}