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, ConfigExt};
5use crate::{
6 DispatchDriftConfig, DriftArgs, DriftType, FeatureMap, FileName, ProfileArgs, ProfileBaseArgs,
7 ProfileRequest, PyHelperFuncs, MISSING,
8};
9
10use chrono::{DateTime, Utc};
11use core::fmt::Debug;
12use potato_head::create_uuid7;
13use pyo3::prelude::*;
14use pyo3::types::PyDict;
15
16use crate::{VersionRequest, DEFAULT_VERSION};
17use scouter_semver::VersionType;
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use std::collections::HashMap;
21use std::path::PathBuf;
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 uid: String,
96
97 #[pyo3(get, set)]
98 pub alert_config: SpcAlertConfig,
99
100 #[pyo3(get)]
101 #[serde(default)]
102 pub feature_map: FeatureMap,
103
104 #[pyo3(get, set)]
105 #[serde(default = "default_drift_type")]
106 pub drift_type: DriftType,
107}
108
109impl ConfigExt for SpcDriftConfig {
110 fn space(&self) -> &str {
111 &self.space
112 }
113
114 fn name(&self) -> &str {
115 &self.name
116 }
117
118 fn version(&self) -> &str {
119 &self.version
120 }
121}
122
123impl Default for SpcDriftConfig {
124 fn default() -> Self {
125 Self {
126 sample_size: 25,
127 sample: true,
128 space: MISSING.to_string(),
129 name: MISSING.to_string(),
130 uid: MISSING.to_string(),
131 version: DEFAULT_VERSION.to_string(),
132 alert_config: SpcAlertConfig::default(),
133 feature_map: FeatureMap::default(),
134 drift_type: DriftType::Spc,
135 }
136 }
137}
138
139fn default_drift_type() -> DriftType {
140 DriftType::Spc
141}
142
143#[pymethods]
144#[allow(clippy::too_many_arguments)]
145impl SpcDriftConfig {
146 #[new]
147 #[pyo3(signature = (space=MISSING, name=MISSING, version=DEFAULT_VERSION, sample=None, sample_size=None, alert_config=None, config_path=None))]
148 pub fn new(
149 space: &str,
150 name: &str,
151 version: &str,
152 sample: Option<bool>,
153 sample_size: Option<usize>,
154 alert_config: Option<SpcAlertConfig>,
155 config_path: Option<PathBuf>,
156 ) -> Result<Self, ProfileError> {
157 if let Some(config_path) = config_path {
158 let config = SpcDriftConfig::load_from_json_file(config_path);
159 return config;
160 }
161
162 let sample = sample.unwrap_or(true);
163 let sample_size = sample_size.unwrap_or(25);
164 let alert_config = alert_config.unwrap_or_default();
165
166 Ok(Self {
167 sample_size,
168 sample,
169 name: name.to_string(),
170 space: space.to_string(),
171 version: version.to_string(),
172 uid: create_uuid7(),
173 alert_config,
174 feature_map: FeatureMap::default(),
175 drift_type: DriftType::Spc,
176 })
177 }
178
179 #[staticmethod]
180 pub fn load_from_json_file(path: PathBuf) -> Result<SpcDriftConfig, ProfileError> {
181 let file = std::fs::read_to_string(&path)?;
184
185 Ok(serde_json::from_str(&file)?)
186 }
187
188 pub fn __str__(&self) -> String {
189 PyHelperFuncs::__str__(self)
191 }
192
193 pub fn model_dump_json(&self) -> String {
194 PyHelperFuncs::__json__(self)
196 }
197
198 #[allow(clippy::too_many_arguments)]
210 #[pyo3(signature = (space=None, name=None, version=None, uid=None, sample=None, sample_size=None, alert_config=None))]
211 pub fn update_config_args(
212 &mut self,
213 space: Option<String>,
214 name: Option<String>,
215 version: Option<String>,
216 uid: Option<String>,
217 sample: Option<bool>,
218 sample_size: Option<usize>,
219 alert_config: Option<SpcAlertConfig>,
220 ) -> Result<(), ProfileError> {
221 if name.is_some() {
222 self.name = name.ok_or(TypeError::MissingNameError)?;
223 }
224
225 if space.is_some() {
226 self.space = space.ok_or(TypeError::MissingSpaceError)?;
227 }
228
229 if version.is_some() {
230 self.version = version.ok_or(TypeError::MissingVersionError)?;
231 }
232
233 if sample.is_some() {
234 self.sample = sample.ok_or(ProfileError::MissingSampleError)?;
235 }
236
237 if sample_size.is_some() {
238 self.sample_size = sample_size.ok_or(ProfileError::MissingSampleSizeError)?;
239 }
240
241 if alert_config.is_some() {
242 self.alert_config = alert_config.ok_or(TypeError::MissingAlertConfigError)?;
243 }
244
245 if uid.is_some() {
246 self.uid = uid.ok_or(TypeError::MissingUidError)?;
247 }
248
249 Ok(())
250 }
251}
252
253impl SpcDriftConfig {
254 pub fn update_feature_map(&mut self, feature_map: FeatureMap) {
255 self.feature_map = feature_map;
256 }
257
258 pub fn load_map_from_json(path: PathBuf) -> Result<HashMap<String, Value>, ProfileError> {
259 let file = std::fs::read_to_string(&path)?;
261 let config = serde_json::from_str(&file)?;
262 Ok(config)
263 }
264}
265
266impl DispatchDriftConfig for SpcDriftConfig {
267 fn get_drift_args(&self) -> DriftArgs {
268 DriftArgs {
269 name: self.name.clone(),
270 space: self.space.clone(),
271 version: self.version.clone(),
272 dispatch_config: self.alert_config.dispatch_config.clone(),
273 }
274 }
275}
276
277#[pyclass]
278#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
279pub struct SpcDriftProfile {
280 #[pyo3(get)]
281 pub features: HashMap<String, SpcFeatureDriftProfile>,
282
283 #[pyo3(get)]
284 pub config: SpcDriftConfig,
285
286 #[pyo3(get)]
287 pub scouter_version: String,
288}
289
290impl SpcDriftProfile {
291 pub fn new(features: HashMap<String, SpcFeatureDriftProfile>, config: SpcDriftConfig) -> Self {
292 Self {
293 features,
294 config,
295 scouter_version: scouter_version(),
296 }
297 }
298}
299
300#[pymethods]
301impl SpcDriftProfile {
302 pub fn __str__(&self) -> String {
303 PyHelperFuncs::__str__(self)
305 }
306
307 pub fn model_dump_json(&self) -> String {
308 PyHelperFuncs::__json__(self)
310 }
311 #[allow(clippy::useless_conversion)]
312 pub fn model_dump(&self, py: Python) -> Result<Py<PyDict>, ProfileError> {
313 let json_str = serde_json::to_string(&self)?;
314
315 let json_value: Value = serde_json::from_str(&json_str)?;
316
317 let dict = PyDict::new(py);
319
320 json_to_pyobject(py, &json_value, &dict)?;
322
323 Ok(dict.into())
325 }
326
327 #[staticmethod]
328 pub fn model_validate(data: &Bound<'_, PyDict>) -> SpcDriftProfile {
329 let json_value = pyobject_to_json(data).unwrap();
330 let string = serde_json::to_string(&json_value).unwrap();
331 serde_json::from_str(&string).expect("Failed to load drift profile")
332 }
333
334 #[staticmethod]
335 pub fn model_validate_json(json_string: String) -> SpcDriftProfile {
336 serde_json::from_str(&json_string).expect("Failed to load monitor profile")
338 }
339
340 #[pyo3(signature = (path=None))]
342 pub fn save_to_json(&self, path: Option<PathBuf>) -> Result<PathBuf, ProfileError> {
343 Ok(PyHelperFuncs::save_to_json(
344 self,
345 path,
346 FileName::SpcDriftProfile.to_str(),
347 )?)
348 }
349
350 #[staticmethod]
351 pub fn from_file(path: PathBuf) -> Result<SpcDriftProfile, ProfileError> {
352 let file = std::fs::read_to_string(&path)?;
353
354 Ok(serde_json::from_str(&file)?)
355 }
356
357 #[allow(clippy::too_many_arguments)]
370 #[pyo3(signature = (space=None, name=None, version=None, uid=None,sample=None, sample_size=None, alert_config=None))]
371 pub fn update_config_args(
372 &mut self,
373 space: Option<String>,
374 name: Option<String>,
375 version: Option<String>,
376 uid: Option<String>,
377 sample: Option<bool>,
378 sample_size: Option<usize>,
379 alert_config: Option<SpcAlertConfig>,
380 ) -> Result<(), ProfileError> {
381 self.config
382 .update_config_args(space, name, version, uid, sample, sample_size, alert_config)
383 }
384
385 pub fn create_profile_request(&self) -> Result<ProfileRequest, TypeError> {
387 let version: Option<String> = if self.config.version == DEFAULT_VERSION {
388 None
389 } else {
390 Some(self.config.version.clone())
391 };
392
393 Ok(ProfileRequest {
394 space: self.config.space.clone(),
395 profile: self.model_dump_json(),
396 drift_type: self.config.drift_type.clone(),
397 version_request: Some(VersionRequest {
398 version,
399 version_type: VersionType::Minor,
400 pre_tag: None,
401 build_tag: None,
402 }),
403 active: false,
404 deactivate_others: false,
405 })
406 }
407
408 #[getter]
409 pub fn uid(&self) -> String {
410 self.config.uid.clone()
411 }
412
413 #[setter]
414 pub fn set_uid(&mut self, uid: String) {
415 self.config.uid = uid;
416 }
417}
418
419impl ProfileBaseArgs for SpcDriftProfile {
420 type Config = SpcDriftConfig;
421
422 fn config(&self) -> &Self::Config {
423 &self.config
424 }
425 fn get_base_args(&self) -> ProfileArgs {
427 ProfileArgs {
428 name: self.config.name.clone(),
429 space: self.config.space.clone(),
430 version: Some(self.config.version.clone()),
431 schedule: self.config.alert_config.schedule.clone(),
432 scouter_version: self.scouter_version.clone(),
433 drift_type: self.config.drift_type.clone(),
434 }
435 }
436
437 fn to_value(&self) -> serde_json::Value {
439 serde_json::to_value(self).unwrap()
440 }
441}
442
443#[cfg(test)]
444mod tests {
445
446 use super::*;
447
448 #[test]
449 fn test_drift_config() {
450 let mut drift_config =
451 SpcDriftConfig::new(MISSING, MISSING, DEFAULT_VERSION, None, None, None, None).unwrap();
452 assert_eq!(drift_config.sample_size, 25);
453 assert!(drift_config.sample);
454 assert_eq!(drift_config.name, "__missing__");
455 assert_eq!(drift_config.space, "__missing__");
456 assert_eq!(drift_config.version, "0.0.0");
457 assert_eq!(drift_config.alert_config, SpcAlertConfig::default());
458
459 drift_config
461 .update_config_args(None, Some("test".to_string()), None, None, None, None, None)
462 .unwrap();
463
464 assert_eq!(drift_config.name, "test");
465 }
466}