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