python_check_updates/
python.rs

1use crate::version::Version;
2use std::process::Command;
3use std::str::FromStr;
4
5/// Information about the Python environment
6#[derive(Debug, Clone)]
7pub struct PythonInfo {
8    /// Current Python version
9    pub current: Version,
10    /// Latest available Python version (from python.org)
11    pub latest: Option<Version>,
12}
13
14impl PythonInfo {
15    /// Check if an update is available
16    pub fn has_update(&self) -> bool {
17        if let Some(ref latest) = self.latest {
18            latest > &self.current
19        } else {
20            false
21        }
22    }
23}
24
25/// Detect the current Python version
26pub fn detect_python_version() -> Option<Version> {
27    // Try python3 first, then python
28    let commands = [
29        ("python3", ["--version"]),
30        ("python", ["--version"]),
31    ];
32
33    for (cmd, args) in &commands {
34        if let Ok(output) = Command::new(cmd).args(args.as_slice()).output() {
35            if output.status.success() {
36                let version_output = String::from_utf8_lossy(&output.stdout);
37                // Output is like "Python 3.11.5"
38                if let Some(version_str) = version_output
39                    .trim()
40                    .strip_prefix("Python ")
41                {
42                    if let Ok(version) = Version::from_str(version_str) {
43                        return Some(version);
44                    }
45                }
46            }
47        }
48    }
49
50    None
51}
52
53/// Fetch the latest Python version from endoflife.date
54pub async fn fetch_latest_python_version() -> Option<Version> {
55    // Use the endoflife.date API - it's well-maintained and returns clean data
56    let url = "https://endoflife.date/api/python.json";
57
58    let client = reqwest::Client::builder()
59        .timeout(std::time::Duration::from_secs(5))
60        .build()
61        .ok()?;
62
63    let response = client.get(url).send().await.ok()?;
64
65    if !response.status().is_success() {
66        return None;
67    }
68
69    let json: serde_json::Value = response.json().await.ok()?;
70
71    // The API returns an array of release cycles sorted by newest first
72    // Each cycle has a "latest" field with the latest version for that cycle
73    let cycles = json.as_array()?;
74
75    // Get the latest version from the first cycle (newest Python release line)
76    // Filter to only consider Python 3.x cycles
77    for cycle in cycles {
78        let cycle_name = cycle.get("cycle")?.as_str()?;
79        if cycle_name.starts_with("3.") {
80            let latest_str = cycle.get("latest")?.as_str()?;
81            if let Ok(version) = Version::from_str(latest_str) {
82                return Some(version);
83            }
84        }
85    }
86
87    None
88}
89
90/// Get Python info (current version and optionally latest available)
91pub async fn get_python_info(check_latest: bool) -> Option<PythonInfo> {
92    let current = detect_python_version()?;
93
94    let latest = if check_latest {
95        fetch_latest_python_version().await
96    } else {
97        None
98    };
99
100    Some(PythonInfo { current, latest })
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_detect_python_version() {
109        // This test depends on Python being installed
110        let version = detect_python_version();
111        // We just check it returns something reasonable
112        if let Some(v) = version {
113            assert!(v.major >= 2);
114        }
115    }
116
117    #[test]
118    fn test_python_info_has_update() {
119        let info = PythonInfo {
120            current: Version::from_str("3.11.0").unwrap(),
121            latest: Some(Version::from_str("3.13.1").unwrap()),
122        };
123        assert!(info.has_update());
124
125        let info = PythonInfo {
126            current: Version::from_str("3.13.1").unwrap(),
127            latest: Some(Version::from_str("3.13.1").unwrap()),
128        };
129        assert!(!info.has_update());
130
131        let info = PythonInfo {
132            current: Version::from_str("3.11.0").unwrap(),
133            latest: None,
134        };
135        assert!(!info.has_update());
136    }
137}