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#[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#[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 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 PyHelperFuncs::__str__(self)
195 }
196
197 pub fn model_dump_json(&self) -> String {
198 PyHelperFuncs::__json__(self)
200 }
201
202 #[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 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 PyHelperFuncs::__str__(self)
309 }
310
311 pub fn model_dump_json(&self) -> String {
312 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 let dict = PyDict::new(py);
323
324 json_to_pyobject(py, &json_value, &dict)?;
326
327 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 serde_json::from_str(&json_string).expect("Failed to load monitor profile")
342 }
343
344 #[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 #[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 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 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 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 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}