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};
5use crate::{
6    DispatchDriftConfig, DriftArgs, DriftType, FeatureMap, FileName, ProfileArgs, ProfileBaseArgs,
7    ProfileFuncs, ProfileRequest, MISSING,
8};
9
10use chrono::{DateTime, Utc};
11use core::fmt::Debug;
12use pyo3::prelude::*;
13use pyo3::types::PyDict;
14
15use crate::{VersionRequest, DEFAULT_VERSION};
16use scouter_semver::VersionType;
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use std::collections::HashMap;
20use std::path::PathBuf;
21use tracing::debug;
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 alert_config: SpcAlertConfig,
96
97    #[pyo3(get)]
98    #[serde(default)]
99    pub feature_map: FeatureMap,
100
101    #[pyo3(get, set)]
102    #[serde(default = "default_drift_type")]
103    pub drift_type: DriftType,
104}
105
106impl Default for SpcDriftConfig {
107    fn default() -> Self {
108        Self {
109            sample_size: 25,
110            sample: true,
111            space: MISSING.to_string(),
112            name: MISSING.to_string(),
113            version: DEFAULT_VERSION.to_string(),
114            alert_config: SpcAlertConfig::default(),
115            feature_map: FeatureMap::default(),
116            drift_type: DriftType::Spc,
117        }
118    }
119}
120
121fn default_drift_type() -> DriftType {
122    DriftType::Spc
123}
124
125#[pymethods]
126#[allow(clippy::too_many_arguments)]
127impl SpcDriftConfig {
128    #[new]
129    #[pyo3(signature = (space=None, name=None, version=None, sample=None, sample_size=None, alert_config=None, config_path=None))]
130    pub fn new(
131        space: Option<String>,
132        name: Option<String>,
133        version: Option<String>,
134        sample: Option<bool>,
135        sample_size: Option<usize>,
136        alert_config: Option<SpcAlertConfig>,
137        config_path: Option<PathBuf>,
138    ) -> Result<Self, ProfileError> {
139        if let Some(config_path) = config_path {
140            let config = SpcDriftConfig::load_from_json_file(config_path);
141            return config;
142        }
143
144        let name = name.unwrap_or(MISSING.to_string());
145        let space = space.unwrap_or(MISSING.to_string());
146
147        if name == MISSING || space == MISSING {
148            debug!("Name and space were not provided. Defaulting to __missing__");
149        }
150
151        let sample = sample.unwrap_or(true);
152        let sample_size = sample_size.unwrap_or(25);
153        let version = version.unwrap_or("0.1.0".to_string());
154        let alert_config = alert_config.unwrap_or_default();
155
156        Ok(Self {
157            sample_size,
158            sample,
159            name,
160            space,
161            version,
162            alert_config,
163            feature_map: FeatureMap::default(),
164            drift_type: DriftType::Spc,
165        })
166    }
167
168    #[staticmethod]
169    pub fn load_from_json_file(path: PathBuf) -> Result<SpcDriftConfig, ProfileError> {
170        // deserialize the string to a struct
171
172        let file = std::fs::read_to_string(&path)?;
173
174        Ok(serde_json::from_str(&file)?)
175    }
176
177    pub fn __str__(&self) -> String {
178        // serialize the struct to a string
179        ProfileFuncs::__str__(self)
180    }
181
182    pub fn model_dump_json(&self) -> String {
183        // serialize the struct to a string
184        ProfileFuncs::__json__(self)
185    }
186
187    // update the arguments of the drift config
188    //
189    // # Arguments
190    //
191    // * `name` - The name of the model
192    // * `space` - The space associated with the model
193    // * `version` - The version of the model
194    // * `sample` - Whether to sample data or not, Default is true
195    // * `sample_size` - The sample size
196    // * `alert_config` - The alerting configuration to use
197    //
198    #[allow(clippy::too_many_arguments)]
199    #[pyo3(signature = (space=None, name=None, version=None, sample=None, sample_size=None, alert_config=None))]
200    pub fn update_config_args(
201        &mut self,
202        space: Option<String>,
203        name: Option<String>,
204        version: Option<String>,
205        sample: Option<bool>,
206        sample_size: Option<usize>,
207        alert_config: Option<SpcAlertConfig>,
208    ) -> Result<(), ProfileError> {
209        if name.is_some() {
210            self.name = name.ok_or(TypeError::MissingNameError)?;
211        }
212
213        if space.is_some() {
214            self.space = space.ok_or(TypeError::MissingSpaceError)?;
215        }
216
217        if version.is_some() {
218            self.version = version.ok_or(TypeError::MissingVersionError)?;
219        }
220
221        if sample.is_some() {
222            self.sample = sample.ok_or(ProfileError::MissingSampleError)?;
223        }
224
225        if sample_size.is_some() {
226            self.sample_size = sample_size.ok_or(ProfileError::MissingSampleSizeError)?;
227        }
228
229        if alert_config.is_some() {
230            self.alert_config = alert_config.ok_or(TypeError::MissingAlertConfigError)?;
231        }
232
233        Ok(())
234    }
235}
236
237impl SpcDriftConfig {
238    pub fn update_feature_map(&mut self, feature_map: FeatureMap) {
239        self.feature_map = feature_map;
240    }
241
242    pub fn load_map_from_json(path: PathBuf) -> Result<HashMap<String, Value>, ProfileError> {
243        // deserialize the string to a struct
244        let file = std::fs::read_to_string(&path)?;
245        let config = serde_json::from_str(&file)?;
246        Ok(config)
247    }
248}
249
250impl DispatchDriftConfig for SpcDriftConfig {
251    fn get_drift_args(&self) -> DriftArgs {
252        DriftArgs {
253            name: self.name.clone(),
254            space: self.space.clone(),
255            version: self.version.clone(),
256            dispatch_config: self.alert_config.dispatch_config.clone(),
257        }
258    }
259}
260
261#[pyclass]
262#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
263pub struct SpcDriftProfile {
264    #[pyo3(get)]
265    pub features: HashMap<String, SpcFeatureDriftProfile>,
266
267    #[pyo3(get)]
268    pub config: SpcDriftConfig,
269
270    #[pyo3(get)]
271    pub scouter_version: String,
272}
273
274impl SpcDriftProfile {
275    pub fn new(features: HashMap<String, SpcFeatureDriftProfile>, config: SpcDriftConfig) -> Self {
276        Self {
277            features,
278            config,
279            scouter_version: scouter_version(),
280        }
281    }
282}
283
284#[pymethods]
285impl SpcDriftProfile {
286    pub fn __str__(&self) -> String {
287        // serialize the struct to a string
288        ProfileFuncs::__str__(self)
289    }
290
291    pub fn model_dump_json(&self) -> String {
292        // serialize the struct to a string
293        ProfileFuncs::__json__(self)
294    }
295    #[allow(clippy::useless_conversion)]
296    pub fn model_dump(&self, py: Python) -> Result<Py<PyDict>, ProfileError> {
297        let json_str = serde_json::to_string(&self)?;
298
299        let json_value: Value = serde_json::from_str(&json_str)?;
300
301        // Create a new Python dictionary
302        let dict = PyDict::new(py);
303
304        // Convert JSON to Python dict
305        json_to_pyobject(py, &json_value, &dict)?;
306
307        // Return the Python dictionary
308        Ok(dict.into())
309    }
310
311    #[staticmethod]
312    pub fn model_validate(data: &Bound<'_, PyDict>) -> SpcDriftProfile {
313        let json_value = pyobject_to_json(data).unwrap();
314
315        let string = serde_json::to_string(&json_value).unwrap();
316        serde_json::from_str(&string).expect("Failed to load drift profile")
317    }
318
319    #[staticmethod]
320    pub fn model_validate_json(json_string: String) -> SpcDriftProfile {
321        // deserialize the string to a struct
322        serde_json::from_str(&json_string).expect("Failed to load monitor profile")
323    }
324
325    // Convert python dict into a drift profile
326    #[pyo3(signature = (path=None))]
327    pub fn save_to_json(&self, path: Option<PathBuf>) -> Result<PathBuf, ProfileError> {
328        Ok(ProfileFuncs::save_to_json(
329            self,
330            path,
331            FileName::SpcDriftProfile.to_str(),
332        )?)
333    }
334
335    #[staticmethod]
336    pub fn from_file(path: PathBuf) -> Result<SpcDriftProfile, ProfileError> {
337        let file = std::fs::read_to_string(&path)?;
338
339        Ok(serde_json::from_str(&file)?)
340    }
341
342    // update the arguments of the drift config
343    //
344    // # Arguments
345    //
346    // * `name` - The name of the model
347    // * `space` - The space associated with the model
348    // * `version` - The version of the model
349    // * `sample` - Whether to sample data or not, Default is true
350    // * `sample_size` - The sample size
351    // * `feature_map` - The feature map to use
352    // * `alert_config` - The alerting configuration to use
353    //
354    #[allow(clippy::too_many_arguments)]
355    #[pyo3(signature = (space=None, name=None, version=None, sample=None, sample_size=None, alert_config=None))]
356    pub fn update_config_args(
357        &mut self,
358        space: Option<String>,
359        name: Option<String>,
360        version: Option<String>,
361        sample: Option<bool>,
362        sample_size: Option<usize>,
363        alert_config: Option<SpcAlertConfig>,
364    ) -> Result<(), ProfileError> {
365        self.config
366            .update_config_args(space, name, version, sample, sample_size, alert_config)
367    }
368
369    /// Create a profile request from the profile
370    pub fn create_profile_request(&self) -> Result<ProfileRequest, TypeError> {
371        let version: Option<String> = if self.config.version == DEFAULT_VERSION {
372            None
373        } else {
374            Some(self.config.version.clone())
375        };
376
377        Ok(ProfileRequest {
378            space: self.config.space.clone(),
379            profile: self.model_dump_json(),
380            drift_type: self.config.drift_type.clone(),
381            version_request: VersionRequest {
382                version,
383                version_type: VersionType::Minor,
384                pre_tag: None,
385                build_tag: None,
386            },
387        })
388    }
389}
390
391impl ProfileBaseArgs for SpcDriftProfile {
392    /// Get the base arguments for the profile (convenience method on the server)
393    fn get_base_args(&self) -> ProfileArgs {
394        ProfileArgs {
395            name: self.config.name.clone(),
396            space: self.config.space.clone(),
397            version: Some(self.config.version.clone()),
398            schedule: self.config.alert_config.schedule.clone(),
399            scouter_version: self.scouter_version.clone(),
400            drift_type: self.config.drift_type.clone(),
401        }
402    }
403
404    /// Convert the struct to a serde_json::Value
405    fn to_value(&self) -> serde_json::Value {
406        serde_json::to_value(self).unwrap()
407    }
408}
409
410#[cfg(test)]
411mod tests {
412
413    use super::*;
414
415    #[test]
416    fn test_drift_config() {
417        let mut drift_config =
418            SpcDriftConfig::new(None, None, None, None, None, None, None).unwrap();
419        assert_eq!(drift_config.sample_size, 25);
420        assert!(drift_config.sample);
421        assert_eq!(drift_config.name, "__missing__");
422        assert_eq!(drift_config.space, "__missing__");
423        assert_eq!(drift_config.version, "0.1.0");
424        assert_eq!(drift_config.alert_config, SpcAlertConfig::default());
425
426        // update
427        drift_config
428            .update_config_args(None, Some("test".to_string()), None, None, None, None)
429            .unwrap();
430
431        assert_eq!(drift_config.name, "test");
432    }
433}