edgefirst_client/
api.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright © 2025 Au-Zone Technologies. All Rights Reserved.
3
4use crate::{AnnotationSet, Client, Dataset, Error, 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
11/// Generic parameter value used in API requests and configuration.
12///
13/// This enum represents various data types that can be passed as parameters
14/// to EdgeFirst Studio API calls or stored in configuration files.
15///
16/// # Examples
17///
18/// ```rust
19/// use edgefirst_client::Parameter;
20/// use std::collections::HashMap;
21///
22/// // Different parameter types
23/// let int_param = Parameter::Integer(42);
24/// let float_param = Parameter::Real(3.14);
25/// let bool_param = Parameter::Boolean(true);
26/// let string_param = Parameter::String("model_name".to_string());
27///
28/// // Complex nested parameters
29/// let array_param = Parameter::Array(vec![
30///     Parameter::Integer(1),
31///     Parameter::Integer(2),
32///     Parameter::Integer(3),
33/// ]);
34///
35/// let mut config = HashMap::new();
36/// config.insert("learning_rate".to_string(), Parameter::Real(0.001));
37/// config.insert("epochs".to_string(), Parameter::Integer(100));
38/// let object_param = Parameter::Object(config);
39/// ```
40#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
41#[serde(untagged)]
42pub enum Parameter {
43    /// 64-bit signed integer value.
44    Integer(i64),
45    /// 64-bit floating-point value.
46    Real(f64),
47    /// Boolean true/false value.
48    Boolean(bool),
49    /// UTF-8 string value.
50    String(String),
51    /// Array of nested parameter values.
52    Array(Vec<Parameter>),
53    /// Object/map with string keys and parameter values.
54    Object(HashMap<String, Parameter>),
55}
56
57#[derive(Deserialize)]
58pub struct LoginResult {
59    pub(crate) token: String,
60}
61
62/// Unique identifier for an organization in EdgeFirst Studio.
63///
64/// Organizations are the top-level containers for users, projects, and
65/// resources in EdgeFirst Studio. Each organization has a unique ID that is
66/// displayed in hexadecimal format with an "org-" prefix (e.g., "org-abc123").
67///
68/// # Examples
69///
70/// ```rust
71/// use edgefirst_client::OrganizationID;
72///
73/// // Create from u64
74/// let org_id = OrganizationID::from(12345);
75/// println!("{}", org_id); // Displays: org-3039
76///
77/// // Parse from string
78/// let org_id: OrganizationID = "org-abc123".try_into().unwrap();
79/// assert_eq!(org_id.value(), 0xabc123);
80/// ```
81#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
82pub struct OrganizationID(u64);
83
84impl Display for OrganizationID {
85    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
86        write!(f, "org-{:x}", self.0)
87    }
88}
89
90impl From<u64> for OrganizationID {
91    fn from(id: u64) -> Self {
92        OrganizationID(id)
93    }
94}
95
96impl From<OrganizationID> for u64 {
97    fn from(val: OrganizationID) -> Self {
98        val.0
99    }
100}
101
102impl OrganizationID {
103    pub fn value(&self) -> u64 {
104        self.0
105    }
106}
107
108impl TryFrom<&str> for OrganizationID {
109    type Error = Error;
110
111    fn try_from(s: &str) -> Result<Self, Self::Error> {
112        let hex_part = s.strip_prefix("org-").ok_or_else(|| {
113            Error::InvalidParameters("Organization ID must start with 'org-' prefix".to_string())
114        })?;
115        let id = u64::from_str_radix(hex_part, 16)?;
116        Ok(OrganizationID(id))
117    }
118}
119
120/// Organization information and metadata.
121///
122/// Each user belongs to an organization which contains projects, datasets,
123/// and other resources. Organizations provide isolated workspaces for teams
124/// and manage resource quotas and billing.
125///
126/// # Examples
127///
128/// ```no_run
129/// use edgefirst_client::{Client, Organization};
130///
131/// # async fn example() -> Result<(), edgefirst_client::Error> {
132/// # let client = Client::new()?;
133/// // Access organization details
134/// let org: Organization = client.organization().await?;
135/// println!("Organization: {} (ID: {})", org.name(), org.id());
136/// println!("Available credits: {}", org.credits());
137/// # Ok(())
138/// # }
139/// ```
140#[derive(Deserialize, Clone, Debug)]
141pub struct Organization {
142    id: OrganizationID,
143    name: String,
144    #[serde(rename = "latest_credit")]
145    credits: i64,
146}
147
148impl Display for Organization {
149    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
150        write!(f, "{}", self.name())
151    }
152}
153
154impl Organization {
155    pub fn id(&self) -> OrganizationID {
156        self.id
157    }
158
159    pub fn name(&self) -> &str {
160        &self.name
161    }
162
163    pub fn credits(&self) -> i64 {
164        self.credits
165    }
166}
167
168/// Unique identifier for a project within EdgeFirst Studio.
169///
170/// Projects contain datasets, experiments, and models within an organization.
171/// Each project has a unique ID displayed in hexadecimal format with a "p-"
172/// prefix (e.g., "p-def456").
173///
174/// # Examples
175///
176/// ```rust
177/// use edgefirst_client::ProjectID;
178/// use std::str::FromStr;
179///
180/// // Create from u64
181/// let project_id = ProjectID::from(78910);
182/// println!("{}", project_id); // Displays: p-1343e
183///
184/// // Parse from string
185/// let project_id = ProjectID::from_str("p-def456").unwrap();
186/// assert_eq!(project_id.value(), 0xdef456);
187/// ```
188#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
189pub struct ProjectID(u64);
190
191impl Display for ProjectID {
192    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
193        write!(f, "p-{:x}", self.0)
194    }
195}
196
197impl From<u64> for ProjectID {
198    fn from(id: u64) -> Self {
199        ProjectID(id)
200    }
201}
202
203impl From<ProjectID> for u64 {
204    fn from(val: ProjectID) -> Self {
205        val.0
206    }
207}
208
209impl ProjectID {
210    pub fn value(&self) -> u64 {
211        self.0
212    }
213}
214
215impl TryFrom<&str> for ProjectID {
216    type Error = Error;
217
218    fn try_from(s: &str) -> Result<Self, Self::Error> {
219        ProjectID::from_str(s)
220    }
221}
222
223impl TryFrom<String> for ProjectID {
224    type Error = Error;
225
226    fn try_from(s: String) -> Result<Self, Self::Error> {
227        ProjectID::from_str(&s)
228    }
229}
230
231impl FromStr for ProjectID {
232    type Err = Error;
233
234    fn from_str(s: &str) -> Result<Self, Self::Err> {
235        let hex_part = s.strip_prefix("p-").ok_or_else(|| {
236            Error::InvalidParameters("Project ID must start with 'p-' prefix".to_string())
237        })?;
238        let id = u64::from_str_radix(hex_part, 16)?;
239        Ok(ProjectID(id))
240    }
241}
242
243/// Unique identifier for an experiment within a project.
244///
245/// Experiments represent individual machine learning experiments with specific
246/// configurations, datasets, and results. Each experiment has a unique ID
247/// displayed in hexadecimal format with an "exp-" prefix (e.g., "exp-123abc").
248///
249/// # Examples
250///
251/// ```rust
252/// use edgefirst_client::ExperimentID;
253/// use std::str::FromStr;
254///
255/// // Create from u64
256/// let exp_id = ExperimentID::from(1193046);
257/// println!("{}", exp_id); // Displays: exp-123abc
258///
259/// // Parse from string
260/// let exp_id = ExperimentID::from_str("exp-456def").unwrap();
261/// assert_eq!(exp_id.value(), 0x456def);
262/// ```
263#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
264pub struct ExperimentID(u64);
265
266impl Display for ExperimentID {
267    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
268        write!(f, "exp-{:x}", self.0)
269    }
270}
271
272impl From<u64> for ExperimentID {
273    fn from(id: u64) -> Self {
274        ExperimentID(id)
275    }
276}
277
278impl From<ExperimentID> for u64 {
279    fn from(val: ExperimentID) -> Self {
280        val.0
281    }
282}
283
284impl ExperimentID {
285    pub fn value(&self) -> u64 {
286        self.0
287    }
288}
289
290impl TryFrom<&str> for ExperimentID {
291    type Error = Error;
292
293    fn try_from(s: &str) -> Result<Self, Self::Error> {
294        ExperimentID::from_str(s)
295    }
296}
297
298impl TryFrom<String> for ExperimentID {
299    type Error = Error;
300
301    fn try_from(s: String) -> Result<Self, Self::Error> {
302        ExperimentID::from_str(&s)
303    }
304}
305
306impl FromStr for ExperimentID {
307    type Err = Error;
308
309    fn from_str(s: &str) -> Result<Self, Self::Err> {
310        let hex_part = s.strip_prefix("exp-").ok_or_else(|| {
311            Error::InvalidParameters("Experiment ID must start with 'exp-' prefix".to_string())
312        })?;
313        let id = u64::from_str_radix(hex_part, 16)?;
314        Ok(ExperimentID(id))
315    }
316}
317
318/// Unique identifier for a training session within an experiment.
319///
320/// Training sessions represent individual training runs with specific
321/// hyperparameters and configurations. Each training session has a unique ID
322/// displayed in hexadecimal format with a "t-" prefix (e.g., "t-789012").
323///
324/// # Examples
325///
326/// ```rust
327/// use edgefirst_client::TrainingSessionID;
328/// use std::str::FromStr;
329///
330/// // Create from u64
331/// let training_id = TrainingSessionID::from(7901234);
332/// println!("{}", training_id); // Displays: t-7872f2
333///
334/// // Parse from string
335/// let training_id = TrainingSessionID::from_str("t-abc123").unwrap();
336/// assert_eq!(training_id.value(), 0xabc123);
337/// ```
338#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
339pub struct TrainingSessionID(u64);
340
341impl Display for TrainingSessionID {
342    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
343        write!(f, "t-{:x}", self.0)
344    }
345}
346
347impl From<u64> for TrainingSessionID {
348    fn from(id: u64) -> Self {
349        TrainingSessionID(id)
350    }
351}
352
353impl From<TrainingSessionID> for u64 {
354    fn from(val: TrainingSessionID) -> Self {
355        val.0
356    }
357}
358
359impl TrainingSessionID {
360    pub fn value(&self) -> u64 {
361        self.0
362    }
363}
364
365impl TryFrom<&str> for TrainingSessionID {
366    type Error = Error;
367
368    fn try_from(s: &str) -> Result<Self, Self::Error> {
369        TrainingSessionID::from_str(s)
370    }
371}
372
373impl TryFrom<String> for TrainingSessionID {
374    type Error = Error;
375
376    fn try_from(s: String) -> Result<Self, Self::Error> {
377        TrainingSessionID::from_str(&s)
378    }
379}
380
381impl FromStr for TrainingSessionID {
382    type Err = Error;
383
384    fn from_str(s: &str) -> Result<Self, Self::Err> {
385        let hex_part = s.strip_prefix("t-").ok_or_else(|| {
386            Error::InvalidParameters("Training Session ID must start with 't-' prefix".to_string())
387        })?;
388        let id = u64::from_str_radix(hex_part, 16)?;
389        Ok(TrainingSessionID(id))
390    }
391}
392
393/// Unique identifier for a validation session within an experiment.
394///
395/// Validation sessions represent model validation runs that evaluate trained
396/// models against test datasets. Each validation session has a unique ID
397/// displayed in hexadecimal format with a "v-" prefix (e.g., "v-345678").
398///
399/// # Examples
400///
401/// ```rust
402/// use edgefirst_client::ValidationSessionID;
403///
404/// // Create from u64
405/// let validation_id = ValidationSessionID::from(3456789);
406/// println!("{}", validation_id); // Displays: v-34c985
407///
408/// // Parse from string
409/// let validation_id: ValidationSessionID = "v-deadbeef".try_into().unwrap();
410/// assert_eq!(validation_id.value(), 0xdeadbeef);
411/// ```
412#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
413pub struct ValidationSessionID(u64);
414
415impl Display for ValidationSessionID {
416    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
417        write!(f, "v-{:x}", self.0)
418    }
419}
420
421impl From<u64> for ValidationSessionID {
422    fn from(id: u64) -> Self {
423        ValidationSessionID(id)
424    }
425}
426
427impl From<ValidationSessionID> for u64 {
428    fn from(val: ValidationSessionID) -> Self {
429        val.0
430    }
431}
432
433impl ValidationSessionID {
434    pub fn value(&self) -> u64 {
435        self.0
436    }
437}
438
439impl TryFrom<&str> for ValidationSessionID {
440    type Error = Error;
441
442    fn try_from(s: &str) -> Result<Self, Self::Error> {
443        let hex_part = s.strip_prefix("v-").ok_or_else(|| {
444            Error::InvalidParameters(
445                "Validation Session ID must start with 'v-' prefix".to_string(),
446            )
447        })?;
448        let id = u64::from_str_radix(hex_part, 16)?;
449        Ok(ValidationSessionID(id))
450    }
451}
452
453impl TryFrom<String> for ValidationSessionID {
454    type Error = Error;
455
456    fn try_from(s: String) -> Result<Self, Self::Error> {
457        ValidationSessionID::try_from(s.as_str())
458    }
459}
460
461#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
462pub struct SnapshotID(u64);
463
464impl Display for SnapshotID {
465    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
466        write!(f, "ss-{:x}", self.0)
467    }
468}
469
470impl From<u64> for SnapshotID {
471    fn from(id: u64) -> Self {
472        SnapshotID(id)
473    }
474}
475
476impl From<SnapshotID> for u64 {
477    fn from(val: SnapshotID) -> Self {
478        val.0
479    }
480}
481
482impl SnapshotID {
483    pub fn value(&self) -> u64 {
484        self.0
485    }
486}
487
488impl TryFrom<&str> for SnapshotID {
489    type Error = Error;
490
491    fn try_from(s: &str) -> Result<Self, Self::Error> {
492        let hex_part = s.strip_prefix("ss-").ok_or_else(|| {
493            Error::InvalidParameters("Snapshot ID must start with 'ss-' prefix".to_string())
494        })?;
495        let id = u64::from_str_radix(hex_part, 16)?;
496        Ok(SnapshotID(id))
497    }
498}
499
500#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
501pub struct TaskID(u64);
502
503impl Display for TaskID {
504    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
505        write!(f, "task-{:x}", self.0)
506    }
507}
508
509impl From<u64> for TaskID {
510    fn from(id: u64) -> Self {
511        TaskID(id)
512    }
513}
514
515impl From<TaskID> for u64 {
516    fn from(val: TaskID) -> Self {
517        val.0
518    }
519}
520
521impl TaskID {
522    pub fn value(&self) -> u64 {
523        self.0
524    }
525}
526
527impl TryFrom<&str> for TaskID {
528    type Error = Error;
529
530    fn try_from(s: &str) -> Result<Self, Self::Error> {
531        TaskID::from_str(s)
532    }
533}
534
535impl TryFrom<String> for TaskID {
536    type Error = Error;
537
538    fn try_from(s: String) -> Result<Self, Self::Error> {
539        TaskID::from_str(&s)
540    }
541}
542
543impl FromStr for TaskID {
544    type Err = Error;
545
546    fn from_str(s: &str) -> Result<Self, Self::Err> {
547        let hex_part = s.strip_prefix("task-").ok_or_else(|| {
548            Error::InvalidParameters("Task ID must start with 'task-' prefix".to_string())
549        })?;
550        let id = u64::from_str_radix(hex_part, 16)?;
551        Ok(TaskID(id))
552    }
553}
554
555/// Unique identifier for a dataset within a project.
556///
557/// Datasets contain collections of images, annotations, and other data used for
558/// machine learning experiments. Each dataset has a unique ID displayed in
559/// hexadecimal format with a "ds-" prefix (e.g., "ds-123abc").
560///
561/// # Examples
562///
563/// ```rust
564/// use edgefirst_client::DatasetID;
565/// use std::str::FromStr;
566///
567/// // Create from u64
568/// let dataset_id = DatasetID::from(1193046);
569/// println!("{}", dataset_id); // Displays: ds-123abc
570///
571/// // Parse from string
572/// let dataset_id = DatasetID::from_str("ds-456def").unwrap();
573/// assert_eq!(dataset_id.value(), 0x456def);
574/// ```
575#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
576pub struct DatasetID(u64);
577
578impl Display for DatasetID {
579    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
580        write!(f, "ds-{:x}", self.0)
581    }
582}
583
584impl From<u64> for DatasetID {
585    fn from(id: u64) -> Self {
586        DatasetID(id)
587    }
588}
589
590impl From<DatasetID> for u64 {
591    fn from(val: DatasetID) -> Self {
592        val.0
593    }
594}
595
596impl DatasetID {
597    pub fn value(&self) -> u64 {
598        self.0
599    }
600}
601
602impl TryFrom<&str> for DatasetID {
603    type Error = Error;
604
605    fn try_from(s: &str) -> Result<Self, Self::Error> {
606        DatasetID::from_str(s)
607    }
608}
609
610impl TryFrom<String> for DatasetID {
611    type Error = Error;
612
613    fn try_from(s: String) -> Result<Self, Self::Error> {
614        DatasetID::from_str(&s)
615    }
616}
617
618impl FromStr for DatasetID {
619    type Err = Error;
620
621    fn from_str(s: &str) -> Result<Self, Self::Err> {
622        let hex_part = s.strip_prefix("ds-").ok_or_else(|| {
623            Error::InvalidParameters("Dataset ID must start with 'ds-' prefix".to_string())
624        })?;
625        let id = u64::from_str_radix(hex_part, 16)?;
626        Ok(DatasetID(id))
627    }
628}
629
630#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
631pub struct AnnotationSetID(u64);
632
633impl Display for AnnotationSetID {
634    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
635        write!(f, "as-{:x}", self.0)
636    }
637}
638
639impl From<u64> for AnnotationSetID {
640    fn from(id: u64) -> Self {
641        AnnotationSetID(id)
642    }
643}
644
645impl From<AnnotationSetID> for u64 {
646    fn from(val: AnnotationSetID) -> Self {
647        val.0
648    }
649}
650
651impl AnnotationSetID {
652    pub fn value(&self) -> u64 {
653        self.0
654    }
655}
656
657impl TryFrom<&str> for AnnotationSetID {
658    type Error = Error;
659
660    fn try_from(s: &str) -> Result<Self, Self::Error> {
661        AnnotationSetID::from_str(s)
662    }
663}
664
665impl TryFrom<String> for AnnotationSetID {
666    type Error = Error;
667
668    fn try_from(s: String) -> Result<Self, Self::Error> {
669        AnnotationSetID::from_str(&s)
670    }
671}
672
673impl FromStr for AnnotationSetID {
674    type Err = Error;
675
676    fn from_str(s: &str) -> Result<Self, Self::Err> {
677        let hex_part = s.strip_prefix("as-").ok_or_else(|| {
678            Error::InvalidParameters("Annotation Set ID must start with 'as-' prefix".to_string())
679        })?;
680        let id = u64::from_str_radix(hex_part, 16)?;
681        Ok(AnnotationSetID(id))
682    }
683}
684
685#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
686pub struct SampleID(u64);
687
688impl Display for SampleID {
689    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
690        write!(f, "s-{:x}", self.0)
691    }
692}
693
694impl From<u64> for SampleID {
695    fn from(id: u64) -> Self {
696        SampleID(id)
697    }
698}
699
700impl From<SampleID> for u64 {
701    fn from(val: SampleID) -> Self {
702        val.0
703    }
704}
705
706impl SampleID {
707    pub fn value(&self) -> u64 {
708        self.0
709    }
710}
711
712impl TryFrom<&str> for SampleID {
713    type Error = Error;
714
715    fn try_from(s: &str) -> Result<Self, Self::Error> {
716        let hex_part = s.strip_prefix("s-").ok_or_else(|| {
717            Error::InvalidParameters("Sample ID must start with 's-' prefix".to_string())
718        })?;
719        let id = u64::from_str_radix(hex_part, 16)?;
720        Ok(SampleID(id))
721    }
722}
723
724#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
725pub struct AppId(u64);
726
727impl Display for AppId {
728    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
729        write!(f, "app-{:x}", self.0)
730    }
731}
732
733impl From<u64> for AppId {
734    fn from(id: u64) -> Self {
735        AppId(id)
736    }
737}
738
739impl From<AppId> for u64 {
740    fn from(val: AppId) -> Self {
741        val.0
742    }
743}
744
745impl AppId {
746    pub fn value(&self) -> u64 {
747        self.0
748    }
749}
750
751impl TryFrom<&str> for AppId {
752    type Error = Error;
753
754    fn try_from(s: &str) -> Result<Self, Self::Error> {
755        let hex_part = s.strip_prefix("app-").ok_or_else(|| {
756            Error::InvalidParameters("App ID must start with 'app-' prefix".to_string())
757        })?;
758        let id = u64::from_str_radix(hex_part, 16)?;
759        Ok(AppId(id))
760    }
761}
762
763#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
764pub struct ImageId(u64);
765
766impl Display for ImageId {
767    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
768        write!(f, "im-{:x}", self.0)
769    }
770}
771
772impl From<u64> for ImageId {
773    fn from(id: u64) -> Self {
774        ImageId(id)
775    }
776}
777
778impl From<ImageId> for u64 {
779    fn from(val: ImageId) -> Self {
780        val.0
781    }
782}
783
784impl ImageId {
785    pub fn value(&self) -> u64 {
786        self.0
787    }
788}
789
790impl TryFrom<&str> for ImageId {
791    type Error = Error;
792
793    fn try_from(s: &str) -> Result<Self, Self::Error> {
794        let hex_part = s.strip_prefix("im-").ok_or_else(|| {
795            Error::InvalidParameters("Image ID must start with 'im-' prefix".to_string())
796        })?;
797        let id = u64::from_str_radix(hex_part, 16)?;
798        Ok(ImageId(id))
799    }
800}
801
802#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
803pub struct SequenceId(u64);
804
805impl Display for SequenceId {
806    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
807        write!(f, "se-{:x}", self.0)
808    }
809}
810
811impl From<u64> for SequenceId {
812    fn from(id: u64) -> Self {
813        SequenceId(id)
814    }
815}
816
817impl From<SequenceId> for u64 {
818    fn from(val: SequenceId) -> Self {
819        val.0
820    }
821}
822
823impl SequenceId {
824    pub fn value(&self) -> u64 {
825        self.0
826    }
827}
828
829impl TryFrom<&str> for SequenceId {
830    type Error = Error;
831
832    fn try_from(s: &str) -> Result<Self, Self::Error> {
833        let hex_part = s.strip_prefix("se-").ok_or_else(|| {
834            Error::InvalidParameters("Sequence ID must start with 'se-' prefix".to_string())
835        })?;
836        let id = u64::from_str_radix(hex_part, 16)?;
837        Ok(SequenceId(id))
838    }
839}
840
841/// The project class represents a project in the EdgeFirst Studio.  A project
842/// contains datasets, experiments, and other resources related to a specific
843/// task or workflow.
844#[derive(Deserialize, Clone, Debug)]
845pub struct Project {
846    id: ProjectID,
847    name: String,
848    description: String,
849}
850
851impl Display for Project {
852    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
853        write!(f, "{} {}", self.id(), self.name())
854    }
855}
856
857impl Project {
858    pub fn id(&self) -> ProjectID {
859        self.id
860    }
861
862    pub fn name(&self) -> &str {
863        &self.name
864    }
865
866    pub fn description(&self) -> &str {
867        &self.description
868    }
869
870    pub async fn datasets(
871        &self,
872        client: &client::Client,
873        name: Option<&str>,
874    ) -> Result<Vec<Dataset>, Error> {
875        client.datasets(self.id, name).await
876    }
877
878    pub async fn experiments(
879        &self,
880        client: &client::Client,
881        name: Option<&str>,
882    ) -> Result<Vec<Experiment>, Error> {
883        client.experiments(self.id, name).await
884    }
885}
886
887#[derive(Deserialize, Debug)]
888pub struct SamplesCountResult {
889    pub total: u64,
890}
891
892#[derive(Serialize, Clone, Debug)]
893pub struct SamplesListParams {
894    pub dataset_id: DatasetID,
895    #[serde(skip_serializing_if = "Option::is_none")]
896    pub annotation_set_id: Option<AnnotationSetID>,
897    #[serde(skip_serializing_if = "Option::is_none")]
898    pub continue_token: Option<String>,
899    #[serde(skip_serializing_if = "Vec::is_empty")]
900    pub types: Vec<String>,
901    #[serde(skip_serializing_if = "Vec::is_empty")]
902    pub group_names: Vec<String>,
903}
904
905#[derive(Deserialize, Debug)]
906pub struct SamplesListResult {
907    pub samples: Vec<Sample>,
908    pub continue_token: Option<String>,
909}
910
911/// Parameters for populating (importing) samples into a dataset.
912///
913/// Used with the `samples.populate2` API to create new samples in a dataset,
914/// optionally with annotations and sensor data files.
915#[derive(Serialize, Clone, Debug)]
916pub struct SamplesPopulateParams {
917    pub dataset_id: DatasetID,
918    #[serde(skip_serializing_if = "Option::is_none")]
919    pub annotation_set_id: Option<AnnotationSetID>,
920    #[serde(skip_serializing_if = "Option::is_none")]
921    pub presigned_urls: Option<bool>,
922    pub samples: Vec<Sample>,
923}
924
925/// Result from the `samples.populate2` API call.
926///
927/// The API returns an array of populated sample results, one for each sample
928/// that was submitted. Each result contains the sample UUID and presigned URLs
929/// for uploading the associated files.
930#[derive(Deserialize, Debug)]
931pub struct SamplesPopulateResult {
932    /// UUID of the sample that was populated
933    pub uuid: String,
934    /// Presigned URLs for uploading files for this sample
935    pub urls: Vec<PresignedUrl>,
936}
937
938/// A presigned URL for uploading a file to S3.
939#[derive(Deserialize, Debug, Clone)]
940pub struct PresignedUrl {
941    /// Filename as specified in the sample
942    pub filename: String,
943    /// S3 key path
944    pub key: String,
945    /// Presigned URL for uploading (PUT request)
946    pub url: String,
947}
948
949#[derive(Deserialize)]
950pub struct Snapshot {
951    id: SnapshotID,
952    description: String,
953    status: String,
954    path: String,
955    #[serde(rename = "date")]
956    created: DateTime<Utc>,
957}
958
959impl Display for Snapshot {
960    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
961        write!(f, "{} {}", self.id, self.description)
962    }
963}
964
965impl Snapshot {
966    pub fn id(&self) -> SnapshotID {
967        self.id
968    }
969
970    pub fn description(&self) -> &str {
971        &self.description
972    }
973
974    pub fn status(&self) -> &str {
975        &self.status
976    }
977
978    pub fn path(&self) -> &str {
979        &self.path
980    }
981
982    pub fn created(&self) -> &DateTime<Utc> {
983        &self.created
984    }
985}
986
987#[derive(Serialize, Debug)]
988pub struct SnapshotRestore {
989    pub project_id: ProjectID,
990    pub snapshot_id: SnapshotID,
991    pub fps: u64,
992    #[serde(rename = "enabled_topics", skip_serializing_if = "Vec::is_empty")]
993    pub topics: Vec<String>,
994    #[serde(rename = "label_names", skip_serializing_if = "Vec::is_empty")]
995    pub autolabel: Vec<String>,
996    #[serde(rename = "depth_gen")]
997    pub autodepth: bool,
998    pub agtg_pipeline: bool,
999    #[serde(skip_serializing_if = "Option::is_none")]
1000    pub dataset_name: Option<String>,
1001    #[serde(skip_serializing_if = "Option::is_none")]
1002    pub dataset_description: Option<String>,
1003}
1004
1005#[derive(Deserialize, Debug)]
1006pub struct SnapshotRestoreResult {
1007    pub id: SnapshotID,
1008    pub description: String,
1009    pub dataset_name: String,
1010    pub dataset_id: DatasetID,
1011    pub annotation_set_id: AnnotationSetID,
1012    #[serde(default)]
1013    pub task_id: Option<TaskID>,
1014    pub date: DateTime<Utc>,
1015}
1016
1017/// Parameters for creating a snapshot from an existing dataset on the server.
1018///
1019/// This is used with the `snapshots.create` RPC to trigger server-side snapshot
1020/// generation from dataset data (images + annotations).
1021#[derive(Serialize, Debug)]
1022pub struct SnapshotCreateFromDataset {
1023    /// Name/description for the snapshot
1024    pub description: String,
1025    /// Dataset ID to create snapshot from
1026    pub dataset_id: DatasetID,
1027    /// Annotation set ID to use for snapshot creation
1028    pub annotation_set_id: AnnotationSetID,
1029}
1030
1031/// Result of creating a snapshot from an existing dataset.
1032///
1033/// Contains the snapshot ID and task ID for monitoring progress.
1034#[derive(Deserialize, Debug)]
1035pub struct SnapshotFromDatasetResult {
1036    /// The created snapshot ID
1037    #[serde(alias = "snapshot_id")]
1038    pub id: SnapshotID,
1039    /// Task ID for monitoring snapshot creation progress
1040    #[serde(default)]
1041    pub task_id: Option<TaskID>,
1042}
1043
1044#[derive(Deserialize)]
1045pub struct Experiment {
1046    id: ExperimentID,
1047    project_id: ProjectID,
1048    name: String,
1049    description: String,
1050}
1051
1052impl Display for Experiment {
1053    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1054        write!(f, "{} {}", self.id, self.name)
1055    }
1056}
1057
1058impl Experiment {
1059    pub fn id(&self) -> ExperimentID {
1060        self.id
1061    }
1062
1063    pub fn project_id(&self) -> ProjectID {
1064        self.project_id
1065    }
1066
1067    pub fn name(&self) -> &str {
1068        &self.name
1069    }
1070
1071    pub fn description(&self) -> &str {
1072        &self.description
1073    }
1074
1075    pub async fn project(&self, client: &client::Client) -> Result<Project, Error> {
1076        client.project(self.project_id).await
1077    }
1078
1079    pub async fn training_sessions(
1080        &self,
1081        client: &client::Client,
1082        name: Option<&str>,
1083    ) -> Result<Vec<TrainingSession>, Error> {
1084        client.training_sessions(self.id, name).await
1085    }
1086}
1087
1088#[derive(Serialize, Debug)]
1089pub struct PublishMetrics {
1090    #[serde(rename = "trainer_session_id", skip_serializing_if = "Option::is_none")]
1091    pub trainer_session_id: Option<TrainingSessionID>,
1092    #[serde(
1093        rename = "validate_session_id",
1094        skip_serializing_if = "Option::is_none"
1095    )]
1096    pub validate_session_id: Option<ValidationSessionID>,
1097    pub metrics: HashMap<String, Parameter>,
1098}
1099
1100#[derive(Deserialize)]
1101struct TrainingSessionParams {
1102    model_params: HashMap<String, Parameter>,
1103    dataset_params: DatasetParams,
1104}
1105
1106#[derive(Deserialize)]
1107pub struct TrainingSession {
1108    id: TrainingSessionID,
1109    #[serde(rename = "trainer_id")]
1110    experiment_id: ExperimentID,
1111    model: String,
1112    name: String,
1113    description: String,
1114    params: TrainingSessionParams,
1115    #[serde(rename = "docker_task")]
1116    task: Task,
1117}
1118
1119impl Display for TrainingSession {
1120    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1121        write!(f, "{} {}", self.id, self.name())
1122    }
1123}
1124
1125impl TrainingSession {
1126    pub fn id(&self) -> TrainingSessionID {
1127        self.id
1128    }
1129
1130    pub fn name(&self) -> &str {
1131        &self.name
1132    }
1133
1134    pub fn description(&self) -> &str {
1135        &self.description
1136    }
1137
1138    pub fn model(&self) -> &str {
1139        &self.model
1140    }
1141
1142    pub fn experiment_id(&self) -> ExperimentID {
1143        self.experiment_id
1144    }
1145
1146    pub fn task(&self) -> Task {
1147        self.task.clone()
1148    }
1149
1150    pub fn model_params(&self) -> &HashMap<String, Parameter> {
1151        &self.params.model_params
1152    }
1153
1154    pub fn dataset_params(&self) -> &DatasetParams {
1155        &self.params.dataset_params
1156    }
1157
1158    pub fn train_group(&self) -> &str {
1159        &self.params.dataset_params.train_group
1160    }
1161
1162    pub fn val_group(&self) -> &str {
1163        &self.params.dataset_params.val_group
1164    }
1165
1166    pub async fn experiment(&self, client: &client::Client) -> Result<Experiment, Error> {
1167        client.experiment(self.experiment_id).await
1168    }
1169
1170    pub async fn dataset(&self, client: &client::Client) -> Result<Dataset, Error> {
1171        client.dataset(self.params.dataset_params.dataset_id).await
1172    }
1173
1174    pub async fn annotation_set(&self, client: &client::Client) -> Result<AnnotationSet, Error> {
1175        client
1176            .annotation_set(self.params.dataset_params.annotation_set_id)
1177            .await
1178    }
1179
1180    pub async fn artifacts(&self, client: &client::Client) -> Result<Vec<Artifact>, Error> {
1181        client.artifacts(self.id).await
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([("trainer_session_id", self.id().value())]);
1197        let resp: Response = client
1198            .rpc("trainer.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: Some(self.id()),
1215            validate_session_id: None,
1216            metrics,
1217        };
1218
1219        let _: String = client
1220            .rpc("trainer.session.metrics".to_owned(), Some(metrics))
1221            .await?;
1222
1223        Ok(())
1224    }
1225
1226    /// Downloads an artifact from the training session.
1227    pub async fn download_artifact(
1228        &self,
1229        client: &client::Client,
1230        filename: &str,
1231    ) -> Result<Vec<u8>, Error> {
1232        client
1233            .fetch(&format!(
1234                "download_model?training_session_id={}&file={}",
1235                self.id().value(),
1236                filename
1237            ))
1238            .await
1239    }
1240
1241    /// Uploads an artifact to the training session.  The filename will
1242    /// be used as the name of the file in the training session while path is
1243    /// the local path to the file to upload.
1244    pub async fn upload_artifact(
1245        &self,
1246        client: &client::Client,
1247        filename: &str,
1248        path: PathBuf,
1249    ) -> Result<(), Error> {
1250        self.upload(client, &[(format!("artifacts/{}", filename), path)])
1251            .await
1252    }
1253
1254    /// Downloads a checkpoint file from the training session.
1255    pub async fn download_checkpoint(
1256        &self,
1257        client: &client::Client,
1258        filename: &str,
1259    ) -> Result<Vec<u8>, Error> {
1260        client
1261            .fetch(&format!(
1262                "download_checkpoint?folder=checkpoints&training_session_id={}&file={}",
1263                self.id().value(),
1264                filename
1265            ))
1266            .await
1267    }
1268
1269    /// Uploads a checkpoint file to the training session.  The filename will
1270    /// be used as the name of the file in the training session while path is
1271    /// the local path to the file to upload.
1272    pub async fn upload_checkpoint(
1273        &self,
1274        client: &client::Client,
1275        filename: &str,
1276        path: PathBuf,
1277    ) -> Result<(), Error> {
1278        self.upload(client, &[(format!("checkpoints/{}", filename), path)])
1279            .await
1280    }
1281
1282    /// Downloads a file from the training session.  Should only be used for
1283    /// text files, binary files must be downloaded using download_artifact or
1284    /// download_checkpoint.
1285    pub async fn download(&self, client: &client::Client, filename: &str) -> Result<String, Error> {
1286        #[derive(Serialize)]
1287        struct DownloadRequest {
1288            session_id: TrainingSessionID,
1289            file_path: String,
1290        }
1291
1292        let params = DownloadRequest {
1293            session_id: self.id(),
1294            file_path: filename.to_string(),
1295        };
1296
1297        client
1298            .rpc("trainer.download.file".to_owned(), Some(params))
1299            .await
1300    }
1301
1302    pub async fn upload(
1303        &self,
1304        client: &client::Client,
1305        files: &[(String, PathBuf)],
1306    ) -> Result<(), Error> {
1307        let mut parts = Form::new().part(
1308            "params",
1309            Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1310        );
1311
1312        for (name, path) in files {
1313            let file_part = Part::file(path).await?.file_name(name.to_owned());
1314            parts = parts.part("file", file_part);
1315        }
1316
1317        let result = client.post_multipart("trainer.upload.files", parts).await?;
1318        trace!("TrainingSession::upload: {:?}", result);
1319        Ok(())
1320    }
1321}
1322
1323#[derive(Deserialize, Clone, Debug)]
1324pub struct ValidationSession {
1325    id: ValidationSessionID,
1326    description: String,
1327    dataset_id: DatasetID,
1328    experiment_id: ExperimentID,
1329    training_session_id: TrainingSessionID,
1330    #[serde(rename = "gt_annotation_set_id")]
1331    annotation_set_id: AnnotationSetID,
1332    #[serde(deserialize_with = "validation_session_params")]
1333    params: HashMap<String, Parameter>,
1334    #[serde(rename = "docker_task")]
1335    task: Task,
1336}
1337
1338fn validation_session_params<'de, D>(
1339    deserializer: D,
1340) -> Result<HashMap<String, Parameter>, D::Error>
1341where
1342    D: Deserializer<'de>,
1343{
1344    #[derive(Deserialize)]
1345    struct ModelParams {
1346        validation: Option<HashMap<String, Parameter>>,
1347    }
1348
1349    #[derive(Deserialize)]
1350    struct ValidateParams {
1351        model: String,
1352    }
1353
1354    #[derive(Deserialize)]
1355    struct Params {
1356        model_params: ModelParams,
1357        validate_params: ValidateParams,
1358    }
1359
1360    let params = Params::deserialize(deserializer)?;
1361    let params = match params.model_params.validation {
1362        Some(mut map) => {
1363            map.insert(
1364                "model".to_string(),
1365                Parameter::String(params.validate_params.model),
1366            );
1367            map
1368        }
1369        None => HashMap::from([(
1370            "model".to_string(),
1371            Parameter::String(params.validate_params.model),
1372        )]),
1373    };
1374
1375    Ok(params)
1376}
1377
1378impl ValidationSession {
1379    pub fn id(&self) -> ValidationSessionID {
1380        self.id
1381    }
1382
1383    pub fn name(&self) -> &str {
1384        self.task.name()
1385    }
1386
1387    pub fn description(&self) -> &str {
1388        &self.description
1389    }
1390
1391    pub fn dataset_id(&self) -> DatasetID {
1392        self.dataset_id
1393    }
1394
1395    pub fn experiment_id(&self) -> ExperimentID {
1396        self.experiment_id
1397    }
1398
1399    pub fn training_session_id(&self) -> TrainingSessionID {
1400        self.training_session_id
1401    }
1402
1403    pub fn annotation_set_id(&self) -> AnnotationSetID {
1404        self.annotation_set_id
1405    }
1406
1407    pub fn params(&self) -> &HashMap<String, Parameter> {
1408        &self.params
1409    }
1410
1411    pub fn task(&self) -> &Task {
1412        &self.task
1413    }
1414
1415    pub async fn metrics(
1416        &self,
1417        client: &client::Client,
1418    ) -> Result<HashMap<String, Parameter>, Error> {
1419        #[derive(Deserialize)]
1420        #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1421        enum Response {
1422            Empty {},
1423            Map(HashMap<String, Parameter>),
1424            String(String),
1425        }
1426
1427        let params = HashMap::from([("validate_session_id", self.id().value())]);
1428        let resp: Response = client
1429            .rpc("validate.session.metrics".to_owned(), Some(params))
1430            .await?;
1431
1432        Ok(match resp {
1433            Response::String(metrics) => serde_json::from_str(&metrics)?,
1434            Response::Map(metrics) => metrics,
1435            Response::Empty {} => HashMap::new(),
1436        })
1437    }
1438
1439    pub async fn set_metrics(
1440        &self,
1441        client: &client::Client,
1442        metrics: HashMap<String, Parameter>,
1443    ) -> Result<(), Error> {
1444        let metrics = PublishMetrics {
1445            trainer_session_id: None,
1446            validate_session_id: Some(self.id()),
1447            metrics,
1448        };
1449
1450        let _: String = client
1451            .rpc("validate.session.metrics".to_owned(), Some(metrics))
1452            .await?;
1453
1454        Ok(())
1455    }
1456
1457    pub async fn upload(
1458        &self,
1459        client: &client::Client,
1460        files: &[(String, PathBuf)],
1461    ) -> Result<(), Error> {
1462        let mut parts = Form::new().part(
1463            "params",
1464            Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1465        );
1466
1467        for (name, path) in files {
1468            let file_part = Part::file(path).await?.file_name(name.to_owned());
1469            parts = parts.part("file", file_part);
1470        }
1471
1472        let result = client
1473            .post_multipart("validate.upload.files", parts)
1474            .await?;
1475        trace!("ValidationSession::upload: {:?}", result);
1476        Ok(())
1477    }
1478}
1479
1480#[derive(Deserialize, Clone, Debug)]
1481pub struct DatasetParams {
1482    dataset_id: DatasetID,
1483    annotation_set_id: AnnotationSetID,
1484    #[serde(rename = "train_group_name")]
1485    train_group: String,
1486    #[serde(rename = "val_group_name")]
1487    val_group: String,
1488}
1489
1490impl DatasetParams {
1491    pub fn dataset_id(&self) -> DatasetID {
1492        self.dataset_id
1493    }
1494
1495    pub fn annotation_set_id(&self) -> AnnotationSetID {
1496        self.annotation_set_id
1497    }
1498
1499    pub fn train_group(&self) -> &str {
1500        &self.train_group
1501    }
1502
1503    pub fn val_group(&self) -> &str {
1504        &self.val_group
1505    }
1506}
1507
1508#[derive(Serialize, Debug, Clone)]
1509pub struct TasksListParams {
1510    #[serde(skip_serializing_if = "Option::is_none")]
1511    pub continue_token: Option<String>,
1512    #[serde(skip_serializing_if = "Option::is_none")]
1513    pub types: Option<Vec<String>>,
1514    #[serde(rename = "manage_types", skip_serializing_if = "Option::is_none")]
1515    pub manager: Option<Vec<String>>,
1516    #[serde(skip_serializing_if = "Option::is_none")]
1517    pub status: Option<Vec<String>>,
1518}
1519
1520#[derive(Deserialize, Debug, Clone)]
1521pub struct TasksListResult {
1522    pub tasks: Vec<Task>,
1523    pub continue_token: Option<String>,
1524}
1525
1526#[derive(Deserialize, Debug, Clone)]
1527pub struct Task {
1528    id: TaskID,
1529    name: String,
1530    #[serde(rename = "type")]
1531    workflow: String,
1532    status: String,
1533    #[serde(rename = "manage_type")]
1534    manager: Option<String>,
1535    #[serde(rename = "instance_type")]
1536    instance: String,
1537    #[serde(rename = "date")]
1538    created: DateTime<Utc>,
1539}
1540
1541impl Task {
1542    pub fn id(&self) -> TaskID {
1543        self.id
1544    }
1545
1546    pub fn name(&self) -> &str {
1547        &self.name
1548    }
1549
1550    pub fn workflow(&self) -> &str {
1551        &self.workflow
1552    }
1553
1554    pub fn status(&self) -> &str {
1555        &self.status
1556    }
1557
1558    pub fn manager(&self) -> Option<&str> {
1559        self.manager.as_deref()
1560    }
1561
1562    pub fn instance(&self) -> &str {
1563        &self.instance
1564    }
1565
1566    pub fn created(&self) -> &DateTime<Utc> {
1567        &self.created
1568    }
1569}
1570
1571impl Display for Task {
1572    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1573        write!(
1574            f,
1575            "{} [{:?} {}] {}",
1576            self.id,
1577            self.manager(),
1578            self.workflow(),
1579            self.name()
1580        )
1581    }
1582}
1583
1584#[derive(Deserialize, Debug)]
1585pub struct TaskInfo {
1586    id: TaskID,
1587    project_id: Option<ProjectID>,
1588    #[serde(rename = "task_description")]
1589    description: String,
1590    #[serde(rename = "type")]
1591    workflow: String,
1592    status: Option<String>,
1593    progress: TaskProgress,
1594    #[serde(rename = "created_date")]
1595    created: DateTime<Utc>,
1596    #[serde(rename = "end_date")]
1597    completed: DateTime<Utc>,
1598}
1599
1600impl Display for TaskInfo {
1601    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1602        write!(f, "{} {}: {}", self.id, self.workflow(), self.description())
1603    }
1604}
1605
1606impl TaskInfo {
1607    pub fn id(&self) -> TaskID {
1608        self.id
1609    }
1610
1611    pub fn project_id(&self) -> Option<ProjectID> {
1612        self.project_id
1613    }
1614
1615    pub fn description(&self) -> &str {
1616        &self.description
1617    }
1618
1619    pub fn workflow(&self) -> &str {
1620        &self.workflow
1621    }
1622
1623    pub fn status(&self) -> &Option<String> {
1624        &self.status
1625    }
1626
1627    pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1628        let t = client.task_status(self.id(), status).await?;
1629        self.status = Some(t.status);
1630        Ok(())
1631    }
1632
1633    pub fn stages(&self) -> HashMap<String, Stage> {
1634        match &self.progress.stages {
1635            Some(stages) => stages.clone(),
1636            None => HashMap::new(),
1637        }
1638    }
1639
1640    pub async fn update_stage(
1641        &mut self,
1642        client: &Client,
1643        stage: &str,
1644        status: &str,
1645        message: &str,
1646        percentage: u8,
1647    ) -> Result<(), Error> {
1648        client
1649            .update_stage(self.id(), stage, status, message, percentage)
1650            .await?;
1651        let t = client.task_info(self.id()).await?;
1652        self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1653        Ok(())
1654    }
1655
1656    pub async fn set_stages(
1657        &mut self,
1658        client: &Client,
1659        stages: &[(&str, &str)],
1660    ) -> Result<(), Error> {
1661        client.set_stages(self.id(), stages).await?;
1662        let t = client.task_info(self.id()).await?;
1663        self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1664        Ok(())
1665    }
1666
1667    pub fn created(&self) -> &DateTime<Utc> {
1668        &self.created
1669    }
1670
1671    pub fn completed(&self) -> &DateTime<Utc> {
1672        &self.completed
1673    }
1674}
1675
1676#[derive(Deserialize, Debug)]
1677pub struct TaskProgress {
1678    stages: Option<HashMap<String, Stage>>,
1679}
1680
1681#[derive(Serialize, Debug, Clone)]
1682pub struct TaskStatus {
1683    #[serde(rename = "docker_task_id")]
1684    pub task_id: TaskID,
1685    pub status: String,
1686}
1687
1688#[derive(Serialize, Deserialize, Debug, Clone)]
1689pub struct Stage {
1690    #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
1691    task_id: Option<TaskID>,
1692    stage: String,
1693    #[serde(skip_serializing_if = "Option::is_none")]
1694    status: Option<String>,
1695    #[serde(skip_serializing_if = "Option::is_none")]
1696    description: Option<String>,
1697    #[serde(skip_serializing_if = "Option::is_none")]
1698    message: Option<String>,
1699    percentage: u8,
1700}
1701
1702impl Stage {
1703    pub fn new(
1704        task_id: Option<TaskID>,
1705        stage: String,
1706        status: Option<String>,
1707        message: Option<String>,
1708        percentage: u8,
1709    ) -> Self {
1710        Stage {
1711            task_id,
1712            stage,
1713            status,
1714            description: None,
1715            message,
1716            percentage,
1717        }
1718    }
1719
1720    pub fn task_id(&self) -> &Option<TaskID> {
1721        &self.task_id
1722    }
1723
1724    pub fn stage(&self) -> &str {
1725        &self.stage
1726    }
1727
1728    pub fn status(&self) -> &Option<String> {
1729        &self.status
1730    }
1731
1732    pub fn description(&self) -> &Option<String> {
1733        &self.description
1734    }
1735
1736    pub fn message(&self) -> &Option<String> {
1737        &self.message
1738    }
1739
1740    pub fn percentage(&self) -> u8 {
1741        self.percentage
1742    }
1743}
1744
1745#[derive(Serialize, Debug)]
1746pub struct TaskStages {
1747    #[serde(rename = "docker_task_id")]
1748    pub task_id: TaskID,
1749    #[serde(skip_serializing_if = "Vec::is_empty")]
1750    pub stages: Vec<HashMap<String, String>>,
1751}
1752
1753#[derive(Deserialize, Debug)]
1754pub struct Artifact {
1755    name: String,
1756    #[serde(rename = "modelType")]
1757    model_type: String,
1758}
1759
1760impl Artifact {
1761    pub fn name(&self) -> &str {
1762        &self.name
1763    }
1764
1765    pub fn model_type(&self) -> &str {
1766        &self.model_type
1767    }
1768}
1769
1770#[cfg(test)]
1771mod tests {
1772    use super::*;
1773
1774    // ========== OrganizationID Tests ==========
1775    #[test]
1776    fn test_organization_id_from_u64() {
1777        let id = OrganizationID::from(12345);
1778        assert_eq!(id.value(), 12345);
1779    }
1780
1781    #[test]
1782    fn test_organization_id_display() {
1783        let id = OrganizationID::from(0xabc123);
1784        assert_eq!(format!("{}", id), "org-abc123");
1785    }
1786
1787    #[test]
1788    fn test_organization_id_try_from_str_valid() {
1789        let id = OrganizationID::try_from("org-abc123").unwrap();
1790        assert_eq!(id.value(), 0xabc123);
1791    }
1792
1793    #[test]
1794    fn test_organization_id_try_from_str_invalid_prefix() {
1795        let result = OrganizationID::try_from("invalid-abc123");
1796        assert!(result.is_err());
1797        match result {
1798            Err(Error::InvalidParameters(msg)) => {
1799                assert!(msg.contains("must start with 'org-'"));
1800            }
1801            _ => panic!("Expected InvalidParameters error"),
1802        }
1803    }
1804
1805    #[test]
1806    fn test_organization_id_try_from_str_invalid_hex() {
1807        let result = OrganizationID::try_from("org-xyz");
1808        assert!(result.is_err());
1809    }
1810
1811    #[test]
1812    fn test_organization_id_try_from_str_empty() {
1813        let result = OrganizationID::try_from("org-");
1814        assert!(result.is_err());
1815    }
1816
1817    #[test]
1818    fn test_organization_id_into_u64() {
1819        let id = OrganizationID::from(54321);
1820        let value: u64 = id.into();
1821        assert_eq!(value, 54321);
1822    }
1823
1824    // ========== ProjectID Tests ==========
1825    #[test]
1826    fn test_project_id_from_u64() {
1827        let id = ProjectID::from(78910);
1828        assert_eq!(id.value(), 78910);
1829    }
1830
1831    #[test]
1832    fn test_project_id_display() {
1833        let id = ProjectID::from(0xdef456);
1834        assert_eq!(format!("{}", id), "p-def456");
1835    }
1836
1837    #[test]
1838    fn test_project_id_from_str_valid() {
1839        let id = ProjectID::from_str("p-def456").unwrap();
1840        assert_eq!(id.value(), 0xdef456);
1841    }
1842
1843    #[test]
1844    fn test_project_id_try_from_str_valid() {
1845        let id = ProjectID::try_from("p-123abc").unwrap();
1846        assert_eq!(id.value(), 0x123abc);
1847    }
1848
1849    #[test]
1850    fn test_project_id_try_from_string_valid() {
1851        let id = ProjectID::try_from("p-456def".to_string()).unwrap();
1852        assert_eq!(id.value(), 0x456def);
1853    }
1854
1855    #[test]
1856    fn test_project_id_from_str_invalid_prefix() {
1857        let result = ProjectID::from_str("proj-123");
1858        assert!(result.is_err());
1859        match result {
1860            Err(Error::InvalidParameters(msg)) => {
1861                assert!(msg.contains("must start with 'p-'"));
1862            }
1863            _ => panic!("Expected InvalidParameters error"),
1864        }
1865    }
1866
1867    #[test]
1868    fn test_project_id_from_str_invalid_hex() {
1869        let result = ProjectID::from_str("p-notahex");
1870        assert!(result.is_err());
1871    }
1872
1873    #[test]
1874    fn test_project_id_into_u64() {
1875        let id = ProjectID::from(99999);
1876        let value: u64 = id.into();
1877        assert_eq!(value, 99999);
1878    }
1879
1880    // ========== ExperimentID Tests ==========
1881    #[test]
1882    fn test_experiment_id_from_u64() {
1883        let id = ExperimentID::from(1193046);
1884        assert_eq!(id.value(), 1193046);
1885    }
1886
1887    #[test]
1888    fn test_experiment_id_display() {
1889        let id = ExperimentID::from(0x123abc);
1890        assert_eq!(format!("{}", id), "exp-123abc");
1891    }
1892
1893    #[test]
1894    fn test_experiment_id_from_str_valid() {
1895        let id = ExperimentID::from_str("exp-456def").unwrap();
1896        assert_eq!(id.value(), 0x456def);
1897    }
1898
1899    #[test]
1900    fn test_experiment_id_try_from_str_valid() {
1901        let id = ExperimentID::try_from("exp-789abc").unwrap();
1902        assert_eq!(id.value(), 0x789abc);
1903    }
1904
1905    #[test]
1906    fn test_experiment_id_try_from_string_valid() {
1907        let id = ExperimentID::try_from("exp-fedcba".to_string()).unwrap();
1908        assert_eq!(id.value(), 0xfedcba);
1909    }
1910
1911    #[test]
1912    fn test_experiment_id_from_str_invalid_prefix() {
1913        let result = ExperimentID::from_str("experiment-123");
1914        assert!(result.is_err());
1915        match result {
1916            Err(Error::InvalidParameters(msg)) => {
1917                assert!(msg.contains("must start with 'exp-'"));
1918            }
1919            _ => panic!("Expected InvalidParameters error"),
1920        }
1921    }
1922
1923    #[test]
1924    fn test_experiment_id_from_str_invalid_hex() {
1925        let result = ExperimentID::from_str("exp-zzz");
1926        assert!(result.is_err());
1927    }
1928
1929    #[test]
1930    fn test_experiment_id_into_u64() {
1931        let id = ExperimentID::from(777777);
1932        let value: u64 = id.into();
1933        assert_eq!(value, 777777);
1934    }
1935
1936    // ========== TrainingSessionID Tests ==========
1937    #[test]
1938    fn test_training_session_id_from_u64() {
1939        let id = TrainingSessionID::from(7901234);
1940        assert_eq!(id.value(), 7901234);
1941    }
1942
1943    #[test]
1944    fn test_training_session_id_display() {
1945        let id = TrainingSessionID::from(0xabc123);
1946        assert_eq!(format!("{}", id), "t-abc123");
1947    }
1948
1949    #[test]
1950    fn test_training_session_id_from_str_valid() {
1951        let id = TrainingSessionID::from_str("t-abc123").unwrap();
1952        assert_eq!(id.value(), 0xabc123);
1953    }
1954
1955    #[test]
1956    fn test_training_session_id_try_from_str_valid() {
1957        let id = TrainingSessionID::try_from("t-deadbeef").unwrap();
1958        assert_eq!(id.value(), 0xdeadbeef);
1959    }
1960
1961    #[test]
1962    fn test_training_session_id_try_from_string_valid() {
1963        let id = TrainingSessionID::try_from("t-cafebabe".to_string()).unwrap();
1964        assert_eq!(id.value(), 0xcafebabe);
1965    }
1966
1967    #[test]
1968    fn test_training_session_id_from_str_invalid_prefix() {
1969        let result = TrainingSessionID::from_str("training-123");
1970        assert!(result.is_err());
1971        match result {
1972            Err(Error::InvalidParameters(msg)) => {
1973                assert!(msg.contains("must start with 't-'"));
1974            }
1975            _ => panic!("Expected InvalidParameters error"),
1976        }
1977    }
1978
1979    #[test]
1980    fn test_training_session_id_from_str_invalid_hex() {
1981        let result = TrainingSessionID::from_str("t-qqq");
1982        assert!(result.is_err());
1983    }
1984
1985    #[test]
1986    fn test_training_session_id_into_u64() {
1987        let id = TrainingSessionID::from(123456);
1988        let value: u64 = id.into();
1989        assert_eq!(value, 123456);
1990    }
1991
1992    // ========== ValidationSessionID Tests ==========
1993    #[test]
1994    fn test_validation_session_id_from_u64() {
1995        let id = ValidationSessionID::from(3456789);
1996        assert_eq!(id.value(), 3456789);
1997    }
1998
1999    #[test]
2000    fn test_validation_session_id_display() {
2001        let id = ValidationSessionID::from(0x34c985);
2002        assert_eq!(format!("{}", id), "v-34c985");
2003    }
2004
2005    #[test]
2006    fn test_validation_session_id_try_from_str_valid() {
2007        let id = ValidationSessionID::try_from("v-deadbeef").unwrap();
2008        assert_eq!(id.value(), 0xdeadbeef);
2009    }
2010
2011    #[test]
2012    fn test_validation_session_id_try_from_string_valid() {
2013        let id = ValidationSessionID::try_from("v-12345678".to_string()).unwrap();
2014        assert_eq!(id.value(), 0x12345678);
2015    }
2016
2017    #[test]
2018    fn test_validation_session_id_try_from_str_invalid_prefix() {
2019        let result = ValidationSessionID::try_from("validation-123");
2020        assert!(result.is_err());
2021        match result {
2022            Err(Error::InvalidParameters(msg)) => {
2023                assert!(msg.contains("must start with 'v-'"));
2024            }
2025            _ => panic!("Expected InvalidParameters error"),
2026        }
2027    }
2028
2029    #[test]
2030    fn test_validation_session_id_try_from_str_invalid_hex() {
2031        let result = ValidationSessionID::try_from("v-xyz");
2032        assert!(result.is_err());
2033    }
2034
2035    #[test]
2036    fn test_validation_session_id_into_u64() {
2037        let id = ValidationSessionID::from(987654);
2038        let value: u64 = id.into();
2039        assert_eq!(value, 987654);
2040    }
2041
2042    // ========== SnapshotID Tests ==========
2043    #[test]
2044    fn test_snapshot_id_from_u64() {
2045        let id = SnapshotID::from(111222);
2046        assert_eq!(id.value(), 111222);
2047    }
2048
2049    #[test]
2050    fn test_snapshot_id_display() {
2051        let id = SnapshotID::from(0xaabbcc);
2052        assert_eq!(format!("{}", id), "ss-aabbcc");
2053    }
2054
2055    #[test]
2056    fn test_snapshot_id_try_from_str_valid() {
2057        let id = SnapshotID::try_from("ss-aabbcc").unwrap();
2058        assert_eq!(id.value(), 0xaabbcc);
2059    }
2060
2061    #[test]
2062    fn test_snapshot_id_try_from_str_invalid_prefix() {
2063        let result = SnapshotID::try_from("snapshot-123");
2064        assert!(result.is_err());
2065        match result {
2066            Err(Error::InvalidParameters(msg)) => {
2067                assert!(msg.contains("must start with 'ss-'"));
2068            }
2069            _ => panic!("Expected InvalidParameters error"),
2070        }
2071    }
2072
2073    #[test]
2074    fn test_snapshot_id_try_from_str_invalid_hex() {
2075        let result = SnapshotID::try_from("ss-ggg");
2076        assert!(result.is_err());
2077    }
2078
2079    #[test]
2080    fn test_snapshot_id_into_u64() {
2081        let id = SnapshotID::from(333444);
2082        let value: u64 = id.into();
2083        assert_eq!(value, 333444);
2084    }
2085
2086    // ========== TaskID Tests ==========
2087    #[test]
2088    fn test_task_id_from_u64() {
2089        let id = TaskID::from(555666);
2090        assert_eq!(id.value(), 555666);
2091    }
2092
2093    #[test]
2094    fn test_task_id_display() {
2095        let id = TaskID::from(0x123456);
2096        assert_eq!(format!("{}", id), "task-123456");
2097    }
2098
2099    #[test]
2100    fn test_task_id_from_str_valid() {
2101        let id = TaskID::from_str("task-123456").unwrap();
2102        assert_eq!(id.value(), 0x123456);
2103    }
2104
2105    #[test]
2106    fn test_task_id_try_from_str_valid() {
2107        let id = TaskID::try_from("task-abcdef").unwrap();
2108        assert_eq!(id.value(), 0xabcdef);
2109    }
2110
2111    #[test]
2112    fn test_task_id_try_from_string_valid() {
2113        let id = TaskID::try_from("task-fedcba".to_string()).unwrap();
2114        assert_eq!(id.value(), 0xfedcba);
2115    }
2116
2117    #[test]
2118    fn test_task_id_from_str_invalid_prefix() {
2119        let result = TaskID::from_str("t-123");
2120        assert!(result.is_err());
2121        match result {
2122            Err(Error::InvalidParameters(msg)) => {
2123                assert!(msg.contains("must start with 'task-'"));
2124            }
2125            _ => panic!("Expected InvalidParameters error"),
2126        }
2127    }
2128
2129    #[test]
2130    fn test_task_id_from_str_invalid_hex() {
2131        let result = TaskID::from_str("task-zzz");
2132        assert!(result.is_err());
2133    }
2134
2135    #[test]
2136    fn test_task_id_into_u64() {
2137        let id = TaskID::from(777888);
2138        let value: u64 = id.into();
2139        assert_eq!(value, 777888);
2140    }
2141
2142    // ========== DatasetID Tests ==========
2143    #[test]
2144    fn test_dataset_id_from_u64() {
2145        let id = DatasetID::from(1193046);
2146        assert_eq!(id.value(), 1193046);
2147    }
2148
2149    #[test]
2150    fn test_dataset_id_display() {
2151        let id = DatasetID::from(0x123abc);
2152        assert_eq!(format!("{}", id), "ds-123abc");
2153    }
2154
2155    #[test]
2156    fn test_dataset_id_from_str_valid() {
2157        let id = DatasetID::from_str("ds-456def").unwrap();
2158        assert_eq!(id.value(), 0x456def);
2159    }
2160
2161    #[test]
2162    fn test_dataset_id_try_from_str_valid() {
2163        let id = DatasetID::try_from("ds-789abc").unwrap();
2164        assert_eq!(id.value(), 0x789abc);
2165    }
2166
2167    #[test]
2168    fn test_dataset_id_try_from_string_valid() {
2169        let id = DatasetID::try_from("ds-fedcba".to_string()).unwrap();
2170        assert_eq!(id.value(), 0xfedcba);
2171    }
2172
2173    #[test]
2174    fn test_dataset_id_from_str_invalid_prefix() {
2175        let result = DatasetID::from_str("dataset-123");
2176        assert!(result.is_err());
2177        match result {
2178            Err(Error::InvalidParameters(msg)) => {
2179                assert!(msg.contains("must start with 'ds-'"));
2180            }
2181            _ => panic!("Expected InvalidParameters error"),
2182        }
2183    }
2184
2185    #[test]
2186    fn test_dataset_id_from_str_invalid_hex() {
2187        let result = DatasetID::from_str("ds-zzz");
2188        assert!(result.is_err());
2189    }
2190
2191    #[test]
2192    fn test_dataset_id_into_u64() {
2193        let id = DatasetID::from(111111);
2194        let value: u64 = id.into();
2195        assert_eq!(value, 111111);
2196    }
2197
2198    // ========== AnnotationSetID Tests ==========
2199    #[test]
2200    fn test_annotation_set_id_from_u64() {
2201        let id = AnnotationSetID::from(222333);
2202        assert_eq!(id.value(), 222333);
2203    }
2204
2205    #[test]
2206    fn test_annotation_set_id_display() {
2207        let id = AnnotationSetID::from(0xabcdef);
2208        assert_eq!(format!("{}", id), "as-abcdef");
2209    }
2210
2211    #[test]
2212    fn test_annotation_set_id_from_str_valid() {
2213        let id = AnnotationSetID::from_str("as-abcdef").unwrap();
2214        assert_eq!(id.value(), 0xabcdef);
2215    }
2216
2217    #[test]
2218    fn test_annotation_set_id_try_from_str_valid() {
2219        let id = AnnotationSetID::try_from("as-123456").unwrap();
2220        assert_eq!(id.value(), 0x123456);
2221    }
2222
2223    #[test]
2224    fn test_annotation_set_id_try_from_string_valid() {
2225        let id = AnnotationSetID::try_from("as-fedcba".to_string()).unwrap();
2226        assert_eq!(id.value(), 0xfedcba);
2227    }
2228
2229    #[test]
2230    fn test_annotation_set_id_from_str_invalid_prefix() {
2231        let result = AnnotationSetID::from_str("annotation-123");
2232        assert!(result.is_err());
2233        match result {
2234            Err(Error::InvalidParameters(msg)) => {
2235                assert!(msg.contains("must start with 'as-'"));
2236            }
2237            _ => panic!("Expected InvalidParameters error"),
2238        }
2239    }
2240
2241    #[test]
2242    fn test_annotation_set_id_from_str_invalid_hex() {
2243        let result = AnnotationSetID::from_str("as-zzz");
2244        assert!(result.is_err());
2245    }
2246
2247    #[test]
2248    fn test_annotation_set_id_into_u64() {
2249        let id = AnnotationSetID::from(444555);
2250        let value: u64 = id.into();
2251        assert_eq!(value, 444555);
2252    }
2253
2254    // ========== SampleID Tests ==========
2255    #[test]
2256    fn test_sample_id_from_u64() {
2257        let id = SampleID::from(666777);
2258        assert_eq!(id.value(), 666777);
2259    }
2260
2261    #[test]
2262    fn test_sample_id_display() {
2263        let id = SampleID::from(0x987654);
2264        assert_eq!(format!("{}", id), "s-987654");
2265    }
2266
2267    #[test]
2268    fn test_sample_id_try_from_str_valid() {
2269        let id = SampleID::try_from("s-987654").unwrap();
2270        assert_eq!(id.value(), 0x987654);
2271    }
2272
2273    #[test]
2274    fn test_sample_id_try_from_str_invalid_prefix() {
2275        let result = SampleID::try_from("sample-123");
2276        assert!(result.is_err());
2277        match result {
2278            Err(Error::InvalidParameters(msg)) => {
2279                assert!(msg.contains("must start with 's-'"));
2280            }
2281            _ => panic!("Expected InvalidParameters error"),
2282        }
2283    }
2284
2285    #[test]
2286    fn test_sample_id_try_from_str_invalid_hex() {
2287        let result = SampleID::try_from("s-zzz");
2288        assert!(result.is_err());
2289    }
2290
2291    #[test]
2292    fn test_sample_id_into_u64() {
2293        let id = SampleID::from(888999);
2294        let value: u64 = id.into();
2295        assert_eq!(value, 888999);
2296    }
2297
2298    // ========== AppId Tests ==========
2299    #[test]
2300    fn test_app_id_from_u64() {
2301        let id = AppId::from(123123);
2302        assert_eq!(id.value(), 123123);
2303    }
2304
2305    #[test]
2306    fn test_app_id_display() {
2307        let id = AppId::from(0x456789);
2308        assert_eq!(format!("{}", id), "app-456789");
2309    }
2310
2311    #[test]
2312    fn test_app_id_try_from_str_valid() {
2313        let id = AppId::try_from("app-456789").unwrap();
2314        assert_eq!(id.value(), 0x456789);
2315    }
2316
2317    #[test]
2318    fn test_app_id_try_from_str_invalid_prefix() {
2319        let result = AppId::try_from("application-123");
2320        assert!(result.is_err());
2321        match result {
2322            Err(Error::InvalidParameters(msg)) => {
2323                assert!(msg.contains("must start with 'app-'"));
2324            }
2325            _ => panic!("Expected InvalidParameters error"),
2326        }
2327    }
2328
2329    #[test]
2330    fn test_app_id_try_from_str_invalid_hex() {
2331        let result = AppId::try_from("app-zzz");
2332        assert!(result.is_err());
2333    }
2334
2335    #[test]
2336    fn test_app_id_into_u64() {
2337        let id = AppId::from(321321);
2338        let value: u64 = id.into();
2339        assert_eq!(value, 321321);
2340    }
2341
2342    // ========== ImageId Tests ==========
2343    #[test]
2344    fn test_image_id_from_u64() {
2345        let id = ImageId::from(789789);
2346        assert_eq!(id.value(), 789789);
2347    }
2348
2349    #[test]
2350    fn test_image_id_display() {
2351        let id = ImageId::from(0xabcd1234);
2352        assert_eq!(format!("{}", id), "im-abcd1234");
2353    }
2354
2355    #[test]
2356    fn test_image_id_try_from_str_valid() {
2357        let id = ImageId::try_from("im-abcd1234").unwrap();
2358        assert_eq!(id.value(), 0xabcd1234);
2359    }
2360
2361    #[test]
2362    fn test_image_id_try_from_str_invalid_prefix() {
2363        let result = ImageId::try_from("image-123");
2364        assert!(result.is_err());
2365        match result {
2366            Err(Error::InvalidParameters(msg)) => {
2367                assert!(msg.contains("must start with 'im-'"));
2368            }
2369            _ => panic!("Expected InvalidParameters error"),
2370        }
2371    }
2372
2373    #[test]
2374    fn test_image_id_try_from_str_invalid_hex() {
2375        let result = ImageId::try_from("im-zzz");
2376        assert!(result.is_err());
2377    }
2378
2379    #[test]
2380    fn test_image_id_into_u64() {
2381        let id = ImageId::from(987987);
2382        let value: u64 = id.into();
2383        assert_eq!(value, 987987);
2384    }
2385
2386    // ========== ID Type Hash and Equality Tests ==========
2387    #[test]
2388    fn test_id_types_equality() {
2389        let id1 = ProjectID::from(12345);
2390        let id2 = ProjectID::from(12345);
2391        let id3 = ProjectID::from(54321);
2392
2393        assert_eq!(id1, id2);
2394        assert_ne!(id1, id3);
2395    }
2396
2397    #[test]
2398    fn test_id_types_hash() {
2399        use std::collections::HashSet;
2400
2401        let mut set = HashSet::new();
2402        set.insert(DatasetID::from(100));
2403        set.insert(DatasetID::from(200));
2404        set.insert(DatasetID::from(100)); // duplicate
2405
2406        assert_eq!(set.len(), 2);
2407        assert!(set.contains(&DatasetID::from(100)));
2408        assert!(set.contains(&DatasetID::from(200)));
2409    }
2410
2411    #[test]
2412    fn test_id_types_copy_clone() {
2413        let id1 = ExperimentID::from(999);
2414        let id2 = id1; // Copy
2415        let id3 = id1; // Also Copy (no need for clone())
2416
2417        assert_eq!(id1, id2);
2418        assert_eq!(id1, id3);
2419    }
2420
2421    // ========== Edge Cases ==========
2422    #[test]
2423    fn test_id_zero_value() {
2424        let id = ProjectID::from(0);
2425        assert_eq!(format!("{}", id), "p-0");
2426        assert_eq!(id.value(), 0);
2427    }
2428
2429    #[test]
2430    fn test_id_max_value() {
2431        let id = ProjectID::from(u64::MAX);
2432        assert_eq!(format!("{}", id), "p-ffffffffffffffff");
2433        assert_eq!(id.value(), u64::MAX);
2434    }
2435
2436    #[test]
2437    fn test_id_round_trip_conversion() {
2438        let original = 0xdeadbeef_u64;
2439        let id = TrainingSessionID::from(original);
2440        let back: u64 = id.into();
2441        assert_eq!(original, back);
2442    }
2443
2444    #[test]
2445    fn test_id_case_insensitive_hex() {
2446        // Hexadecimal parsing should handle both upper and lowercase
2447        let id1 = DatasetID::from_str("ds-ABCDEF").unwrap();
2448        let id2 = DatasetID::from_str("ds-abcdef").unwrap();
2449        assert_eq!(id1.value(), id2.value());
2450    }
2451
2452    #[test]
2453    fn test_id_with_leading_zeros() {
2454        let id = ProjectID::from_str("p-00001234").unwrap();
2455        assert_eq!(id.value(), 0x1234);
2456    }
2457
2458    // ========== Parameter Tests ==========
2459    #[test]
2460    fn test_parameter_integer() {
2461        let param = Parameter::Integer(42);
2462        match param {
2463            Parameter::Integer(val) => assert_eq!(val, 42),
2464            _ => panic!("Expected Integer variant"),
2465        }
2466    }
2467
2468    #[test]
2469    fn test_parameter_real() {
2470        let param = Parameter::Real(2.5);
2471        match param {
2472            Parameter::Real(val) => assert_eq!(val, 2.5),
2473            _ => panic!("Expected Real variant"),
2474        }
2475    }
2476
2477    #[test]
2478    fn test_parameter_boolean() {
2479        let param = Parameter::Boolean(true);
2480        match param {
2481            Parameter::Boolean(val) => assert!(val),
2482            _ => panic!("Expected Boolean variant"),
2483        }
2484    }
2485
2486    #[test]
2487    fn test_parameter_string() {
2488        let param = Parameter::String("test".to_string());
2489        match param {
2490            Parameter::String(val) => assert_eq!(val, "test"),
2491            _ => panic!("Expected String variant"),
2492        }
2493    }
2494
2495    #[test]
2496    fn test_parameter_array() {
2497        let param = Parameter::Array(vec![
2498            Parameter::Integer(1),
2499            Parameter::Integer(2),
2500            Parameter::Integer(3),
2501        ]);
2502        match param {
2503            Parameter::Array(arr) => assert_eq!(arr.len(), 3),
2504            _ => panic!("Expected Array variant"),
2505        }
2506    }
2507
2508    #[test]
2509    fn test_parameter_object() {
2510        let mut map = HashMap::new();
2511        map.insert("key".to_string(), Parameter::Integer(100));
2512        let param = Parameter::Object(map);
2513        match param {
2514            Parameter::Object(obj) => {
2515                assert_eq!(obj.len(), 1);
2516                assert!(obj.contains_key("key"));
2517            }
2518            _ => panic!("Expected Object variant"),
2519        }
2520    }
2521
2522    #[test]
2523    fn test_parameter_clone() {
2524        let param1 = Parameter::Integer(42);
2525        let param2 = param1.clone();
2526        assert_eq!(param1, param2);
2527    }
2528
2529    #[test]
2530    fn test_parameter_nested() {
2531        let inner_array = Parameter::Array(vec![Parameter::Integer(1), Parameter::Integer(2)]);
2532        let outer_array = Parameter::Array(vec![inner_array.clone(), inner_array]);
2533
2534        match outer_array {
2535            Parameter::Array(arr) => {
2536                assert_eq!(arr.len(), 2);
2537            }
2538            _ => panic!("Expected Array variant"),
2539        }
2540    }
2541}