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#[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#[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 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 ProfileFuncs::__str__(self)
163 }
164
165 pub fn model_dump_json(&self) -> String {
166 ProfileFuncs::__json__(self)
168 }
169
170 #[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 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 ProfileFuncs::__str__(self)
277 }
278
279 pub fn model_dump_json(&self) -> String {
280 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 let dict = PyDict::new(py);
291
292 json_to_pyobject(py, &json_value, &dict)?;
294
295 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 serde_json::from_str(&json_string).expect("Failed to load monitor profile")
311 }
312
313 #[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 #[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 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 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 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 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}