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#[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#[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 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 ProfileFuncs::__str__(self)
180 }
181
182 pub fn model_dump_json(&self) -> String {
183 ProfileFuncs::__json__(self)
185 }
186
187 #[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 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 ProfileFuncs::__str__(self)
289 }
290
291 pub fn model_dump_json(&self) -> String {
292 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 let dict = PyDict::new(py);
303
304 json_to_pyobject(py, &json_value, &dict)?;
306
307 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 serde_json::from_str(&json_string).expect("Failed to load monitor profile")
323 }
324
325 #[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 #[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 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 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 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 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}