Skip to main content

scouter_types/spc/
profile.rs

1#![allow(clippy::useless_conversion)]
2use crate::error::{ProfileError, TypeError};
3use crate::spc::alert::SpcAlertConfig;
4use crate::util::{json_to_pyobject, pyobject_to_json, scouter_version, ConfigExt};
5use crate::{
6    DispatchDriftConfig, DriftArgs, DriftType, FeatureMap, FileName, ProfileArgs, ProfileBaseArgs,
7    ProfileRequest, PyHelperFuncs, MISSING,
8};
9
10use chrono::{DateTime, Utc};
11use core::fmt::Debug;
12use potato_head::create_uuid7;
13use pyo3::prelude::*;
14use pyo3::types::PyDict;
15
16use crate::{VersionRequest, DEFAULT_VERSION};
17use scouter_semver::VersionType;
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use std::collections::HashMap;
21use std::path::PathBuf;
22
23/// Python class for a monitoring profile
24///
25/// # Arguments
26///
27/// * `id` - The id value
28/// * `center` - The center value
29/// * `ucl` - The upper control limit
30/// * `lcl` - The lower control limit
31/// * `timestamp` - The timestamp value
32///
33#[pyclass]
34#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
35pub struct SpcFeatureDriftProfile {
36    #[pyo3(get)]
37    pub id: String,
38
39    #[pyo3(get)]
40    pub center: f64,
41
42    #[pyo3(get)]
43    pub one_ucl: f64,
44
45    #[pyo3(get)]
46    pub one_lcl: f64,
47
48    #[pyo3(get)]
49    pub two_ucl: f64,
50
51    #[pyo3(get)]
52    pub two_lcl: f64,
53
54    #[pyo3(get)]
55    pub three_ucl: f64,
56
57    #[pyo3(get)]
58    pub three_lcl: f64,
59
60    #[pyo3(get)]
61    pub timestamp: DateTime<Utc>,
62}
63
64/// Python class for a monitoring configuration
65///
66/// # Arguments
67///
68/// * `sample_size` - The sample size
69/// * `sample` - Whether to sample data or not, Default is true
70/// * `name` - The name of the model
71/// * `space` - The space associated with the model
72/// * `version` - The version of the model
73/// * `schedule` - The cron schedule for monitoring
74/// * `alert_rule` - The alerting rule to use for monitoring
75///
76#[pyclass]
77#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
78pub struct SpcDriftConfig {
79    #[pyo3(get, set)]
80    pub sample_size: usize,
81
82    #[pyo3(get, set)]
83    pub sample: bool,
84
85    #[pyo3(get, set)]
86    pub space: String,
87
88    #[pyo3(get, set)]
89    pub name: String,
90
91    #[pyo3(get, set)]
92    pub version: String,
93
94    #[pyo3(get, set)]
95    pub uid: String,
96
97    #[pyo3(get, set)]
98    pub alert_config: SpcAlertConfig,
99
100    #[pyo3(get)]
101    #[serde(default)]
102    pub feature_map: FeatureMap,
103
104    #[pyo3(get, set)]
105    #[serde(default = "default_drift_type")]
106    pub drift_type: DriftType,
107}
108
109impl ConfigExt for SpcDriftConfig {
110    fn space(&self) -> &str {
111        &self.space
112    }
113
114    fn name(&self) -> &str {
115        &self.name
116    }
117
118    fn version(&self) -> &str {
119        &self.version
120    }
121}
122
123impl Default for SpcDriftConfig {
124    fn default() -> Self {
125        Self {
126            sample_size: 25,
127            sample: true,
128            space: MISSING.to_string(),
129            name: MISSING.to_string(),
130            uid: MISSING.to_string(),
131            version: DEFAULT_VERSION.to_string(),
132            alert_config: SpcAlertConfig::default(),
133            feature_map: FeatureMap::default(),
134            drift_type: DriftType::Spc,
135        }
136    }
137}
138
139fn default_drift_type() -> DriftType {
140    DriftType::Spc
141}
142
143#[pymethods]
144#[allow(clippy::too_many_arguments)]
145impl SpcDriftConfig {
146    #[new]
147    #[pyo3(signature = (space=MISSING, name=MISSING, version=DEFAULT_VERSION, sample=None, sample_size=None, alert_config=None, config_path=None))]
148    pub fn new(
149        space: &str,
150        name: &str,
151        version: &str,
152        sample: Option<bool>,
153        sample_size: Option<usize>,
154        alert_config: Option<SpcAlertConfig>,
155        config_path: Option<PathBuf>,
156    ) -> Result<Self, ProfileError> {
157        if let Some(config_path) = config_path {
158            let config = SpcDriftConfig::load_from_json_file(config_path);
159            return config;
160        }
161
162        let sample = sample.unwrap_or(true);
163        let sample_size = sample_size.unwrap_or(25);
164        let alert_config = alert_config.unwrap_or_default();
165
166        Ok(Self {
167            sample_size,
168            sample,
169            name: name.to_string(),
170            space: space.to_string(),
171            version: version.to_string(),
172            uid: create_uuid7(),
173            alert_config,
174            feature_map: FeatureMap::default(),
175            drift_type: DriftType::Spc,
176        })
177    }
178
179    #[staticmethod]
180    pub fn load_from_json_file(path: PathBuf) -> Result<SpcDriftConfig, ProfileError> {
181        // deserialize the string to a struct
182
183        let file = std::fs::read_to_string(&path)?;
184
185        Ok(serde_json::from_str(&file)?)
186    }
187
188    pub fn __str__(&self) -> String {
189        // serialize the struct to a string
190        PyHelperFuncs::__str__(self)
191    }
192
193    pub fn model_dump_json(&self) -> String {
194        // serialize the struct to a string
195        PyHelperFuncs::__json__(self)
196    }
197
198    // update the arguments of the drift config
199    //
200    // # Arguments
201    //
202    // * `name` - The name of the model
203    // * `space` - The space associated with the model
204    // * `version` - The version of the model
205    // * `sample` - Whether to sample data or not, Default is true
206    // * `sample_size` - The sample size
207    // * `alert_config` - The alerting configuration to use
208    //
209    #[allow(clippy::too_many_arguments)]
210    #[pyo3(signature = (space=None, name=None, version=None, uid=None, sample=None, sample_size=None, alert_config=None))]
211    pub fn update_config_args(
212        &mut self,
213        space: Option<String>,
214        name: Option<String>,
215        version: Option<String>,
216        uid: Option<String>,
217        sample: Option<bool>,
218        sample_size: Option<usize>,
219        alert_config: Option<SpcAlertConfig>,
220    ) -> Result<(), ProfileError> {
221        if name.is_some() {
222            self.name = name.ok_or(TypeError::MissingNameError)?;
223        }
224
225        if space.is_some() {
226            self.space = space.ok_or(TypeError::MissingSpaceError)?;
227        }
228
229        if version.is_some() {
230            self.version = version.ok_or(TypeError::MissingVersionError)?;
231        }
232
233        if sample.is_some() {
234            self.sample = sample.ok_or(ProfileError::MissingSampleError)?;
235        }
236
237        if sample_size.is_some() {
238            self.sample_size = sample_size.ok_or(ProfileError::MissingSampleSizeError)?;
239        }
240
241        if alert_config.is_some() {
242            self.alert_config = alert_config.ok_or(TypeError::MissingAlertConfigError)?;
243        }
244
245        if uid.is_some() {
246            self.uid = uid.ok_or(TypeError::MissingUidError)?;
247        }
248
249        Ok(())
250    }
251}
252
253impl SpcDriftConfig {
254    pub fn update_feature_map(&mut self, feature_map: FeatureMap) {
255        self.feature_map = feature_map;
256    }
257
258    pub fn load_map_from_json(path: PathBuf) -> Result<HashMap<String, Value>, ProfileError> {
259        // deserialize the string to a struct
260        let file = std::fs::read_to_string(&path)?;
261        let config = serde_json::from_str(&file)?;
262        Ok(config)
263    }
264}
265
266impl DispatchDriftConfig for SpcDriftConfig {
267    fn get_drift_args(&self) -> DriftArgs {
268        DriftArgs {
269            name: self.name.clone(),
270            space: self.space.clone(),
271            version: self.version.clone(),
272            dispatch_config: self.alert_config.dispatch_config.clone(),
273        }
274    }
275}
276
277#[pyclass]
278#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
279pub struct SpcDriftProfile {
280    #[pyo3(get)]
281    pub features: HashMap<String, SpcFeatureDriftProfile>,
282
283    #[pyo3(get)]
284    pub config: SpcDriftConfig,
285
286    #[pyo3(get)]
287    pub scouter_version: String,
288}
289
290impl SpcDriftProfile {
291    pub fn new(features: HashMap<String, SpcFeatureDriftProfile>, config: SpcDriftConfig) -> Self {
292        Self {
293            features,
294            config,
295            scouter_version: scouter_version(),
296        }
297    }
298}
299
300#[pymethods]
301impl SpcDriftProfile {
302    pub fn __str__(&self) -> String {
303        // serialize the struct to a string
304        PyHelperFuncs::__str__(self)
305    }
306
307    pub fn model_dump_json(&self) -> String {
308        // serialize the struct to a string
309        PyHelperFuncs::__json__(self)
310    }
311    #[allow(clippy::useless_conversion)]
312    pub fn model_dump(&self, py: Python) -> Result<Py<PyDict>, ProfileError> {
313        let json_str = serde_json::to_string(&self)?;
314
315        let json_value: Value = serde_json::from_str(&json_str)?;
316
317        // Create a new Python dictionary
318        let dict = PyDict::new(py);
319
320        // Convert JSON to Python dict
321        json_to_pyobject(py, &json_value, &dict)?;
322
323        // Return the Python dictionary
324        Ok(dict.into())
325    }
326
327    #[staticmethod]
328    pub fn model_validate(data: &Bound<'_, PyDict>) -> SpcDriftProfile {
329        let json_value = pyobject_to_json(data).unwrap();
330        let string = serde_json::to_string(&json_value).unwrap();
331        serde_json::from_str(&string).expect("Failed to load drift profile")
332    }
333
334    #[staticmethod]
335    pub fn model_validate_json(json_string: String) -> SpcDriftProfile {
336        // deserialize the string to a struct
337        serde_json::from_str(&json_string).expect("Failed to load monitor profile")
338    }
339
340    // Convert python dict into a drift profile
341    #[pyo3(signature = (path=None))]
342    pub fn save_to_json(&self, path: Option<PathBuf>) -> Result<PathBuf, ProfileError> {
343        Ok(PyHelperFuncs::save_to_json(
344            self,
345            path,
346            FileName::SpcDriftProfile.to_str(),
347        )?)
348    }
349
350    #[staticmethod]
351    pub fn from_file(path: PathBuf) -> Result<SpcDriftProfile, ProfileError> {
352        let file = std::fs::read_to_string(&path)?;
353
354        Ok(serde_json::from_str(&file)?)
355    }
356
357    // update the arguments of the drift config
358    //
359    // # Arguments
360    //
361    // * `name` - The name of the model
362    // * `space` - The space associated with the model
363    // * `version` - The version of the model
364    // * `sample` - Whether to sample data or not, Default is true
365    // * `sample_size` - The sample size
366    // * `feature_map` - The feature map to use
367    // * `alert_config` - The alerting configuration to use
368    //
369    #[allow(clippy::too_many_arguments)]
370    #[pyo3(signature = (space=None, name=None, version=None, uid=None,sample=None, sample_size=None, alert_config=None))]
371    pub fn update_config_args(
372        &mut self,
373        space: Option<String>,
374        name: Option<String>,
375        version: Option<String>,
376        uid: Option<String>,
377        sample: Option<bool>,
378        sample_size: Option<usize>,
379        alert_config: Option<SpcAlertConfig>,
380    ) -> Result<(), ProfileError> {
381        self.config
382            .update_config_args(space, name, version, uid, sample, sample_size, alert_config)
383    }
384
385    /// Create a profile request from the profile
386    pub fn create_profile_request(&self) -> Result<ProfileRequest, TypeError> {
387        let version: Option<String> = if self.config.version == DEFAULT_VERSION {
388            None
389        } else {
390            Some(self.config.version.clone())
391        };
392
393        Ok(ProfileRequest {
394            space: self.config.space.clone(),
395            profile: self.model_dump_json(),
396            drift_type: self.config.drift_type.clone(),
397            version_request: Some(VersionRequest {
398                version,
399                version_type: VersionType::Minor,
400                pre_tag: None,
401                build_tag: None,
402            }),
403            active: false,
404            deactivate_others: false,
405        })
406    }
407
408    #[getter]
409    pub fn uid(&self) -> String {
410        self.config.uid.clone()
411    }
412
413    #[setter]
414    pub fn set_uid(&mut self, uid: String) {
415        self.config.uid = uid;
416    }
417}
418
419impl ProfileBaseArgs for SpcDriftProfile {
420    type Config = SpcDriftConfig;
421
422    fn config(&self) -> &Self::Config {
423        &self.config
424    }
425    /// Get the base arguments for the profile (convenience method on the server)
426    fn get_base_args(&self) -> ProfileArgs {
427        ProfileArgs {
428            name: self.config.name.clone(),
429            space: self.config.space.clone(),
430            version: Some(self.config.version.clone()),
431            schedule: self.config.alert_config.schedule.clone(),
432            scouter_version: self.scouter_version.clone(),
433            drift_type: self.config.drift_type.clone(),
434        }
435    }
436
437    /// Convert the struct to a serde_json::Value
438    fn to_value(&self) -> serde_json::Value {
439        serde_json::to_value(self).unwrap()
440    }
441}
442
443#[cfg(test)]
444mod tests {
445
446    use super::*;
447
448    #[test]
449    fn test_drift_config() {
450        let mut drift_config =
451            SpcDriftConfig::new(MISSING, MISSING, DEFAULT_VERSION, None, None, None, None).unwrap();
452        assert_eq!(drift_config.sample_size, 25);
453        assert!(drift_config.sample);
454        assert_eq!(drift_config.name, "__missing__");
455        assert_eq!(drift_config.space, "__missing__");
456        assert_eq!(drift_config.version, "0.0.0");
457        assert_eq!(drift_config.alert_config, SpcAlertConfig::default());
458
459        // update
460        drift_config
461            .update_config_args(None, Some("test".to_string()), None, None, None, None, None)
462            .unwrap();
463
464        assert_eq!(drift_config.name, "test");
465    }
466}