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