lean_ctx_patterns/
mlflow.rs1use crate::ansi::strip_ansi;
12use std::collections::HashSet;
13
14pub fn compress(_cmd: &str, output: &str) -> Option<String> {
15 let trimmed = output.trim();
16 if trimmed.is_empty() {
17 return Some("mlflow: ok".to_string());
18 }
19
20 let mut kept: Vec<String> = Vec::new();
21 let mut seen: HashSet<String> = HashSet::new();
22
23 for raw in trimmed.lines() {
24 let stripped = strip_ansi(raw);
25 let body = strip_log_prefix(stripped.trim());
26 if body.is_empty() || is_noise(body) {
27 continue;
28 }
29 if seen.insert(normalize(body)) {
30 kept.push(body.to_string());
31 }
32 }
33
34 if kept.is_empty() {
35 return Some("mlflow: ok".to_string());
36 }
37 Some(kept.join("\n"))
38}
39
40fn strip_log_prefix(line: &str) -> &str {
42 let mut it = line.splitn(4, ' ');
43 let (Some(date), Some(time), Some(level), rest) = (it.next(), it.next(), it.next(), it.next())
44 else {
45 return line;
46 };
47 if is_date(date) && is_time(time) && is_level(level) {
48 rest.unwrap_or("").trim_start()
49 } else {
50 line
51 }
52}
53
54fn is_date(s: &str) -> bool {
55 let p: Vec<&str> = s.split('/').collect();
56 p.len() == 3
57 && p.iter()
58 .all(|x| !x.is_empty() && x.chars().all(|c| c.is_ascii_digit()))
59}
60
61fn is_time(s: &str) -> bool {
62 let p: Vec<&str> = s.split(':').collect();
63 p.len() == 3
64 && p.iter()
65 .all(|x| !x.is_empty() && x.chars().all(|c| c.is_ascii_digit()))
66}
67
68fn is_level(s: &str) -> bool {
69 matches!(
70 s,
71 "INFO" | "WARNING" | "WARN" | "ERROR" | "DEBUG" | "CRITICAL"
72 )
73}
74
75fn is_noise(line: &str) -> bool {
76 const PREFIXES: [&str; 9] = [
77 "Collecting ",
78 "Requirement already satisfied",
79 "Downloading ",
80 "Installing ",
81 "Building wheel",
82 "Preparing metadata",
83 "Using cached ",
84 "Channels:",
85 "Platform:",
86 ];
87 if PREFIXES.iter().any(|p| line.starts_with(p)) {
88 return true;
89 }
90 line.contains("Solving environment")
91 || line.contains("MB/s")
92 || line.contains("kB/s")
93 || line.contains("━")
94 || line.contains("it/s]")
95}
96
97fn normalize(s: &str) -> String {
100 s.chars().filter(|c| !c.is_ascii_whitespace()).collect()
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 const RUN: &str = "2024/01/01 12:00:00 INFO mlflow.projects.utils: === Created directory /tmp/x ===\n2024/01/01 12:00:01 INFO mlflow.projects.backend.local: === Running command 'python train.py' ===\nCollecting numpy==1.26.0\nDownloading numpy-1.26.0.whl (18.2 MB)\n ━━━━━━━━━━ 18.2/18.2 MB 25.1 MB/s\nRequirement already satisfied: scipy in /usr/lib\n2024/01/01 12:00:30 INFO mlflow.projects: === Run (ID 'abc123def456') succeeded ===\n";
108
109 #[test]
110 fn strips_prefix_and_env_noise_keeps_lifecycle() {
111 let r = compress("mlflow run .", RUN).unwrap();
112 assert!(r.contains("Run (ID 'abc123def456') succeeded"), "{r}");
113 assert!(r.contains("Running command"), "{r}");
114 assert!(!r.contains("2024/01/01"), "drops log timestamp: {r}");
115 assert!(!r.contains("Collecting"), "drops pip noise: {r}");
116 assert!(!r.contains("MB/s"), "drops download progress: {r}");
117 }
118
119 #[test]
120 fn shorter_than_input() {
121 let r = compress("mlflow run .", RUN).unwrap();
122 assert!(r.len() < RUN.len());
123 }
124
125 #[test]
126 fn empty_is_ok() {
127 assert_eq!(compress("mlflow run .", "").unwrap(), "mlflow: ok");
128 }
129}