asimov_module_cli/registry/
pypi.rs

1// This is free and unencumbered software released into the public domain.
2
3use super::{ModuleMetadata, ModuleType, http::http_client};
4use known_types_pypi::PackageMetadata;
5use reqwest::Error;
6
7/// Fetches JSON metadata for the current `asimov-modules` package.
8pub async fn fetch_current_modules() -> Result<String, Error> {
9    fetch_modules("25.0.0.dev0").await // FIXME
10}
11
12/// Fetches JSON metadata for a specific `asimov-modules` package version.
13pub async fn fetch_modules(version: &str) -> Result<String, Error> {
14    let url = format!("https://pypi.org/pypi/asimov-modules/{}/json", version);
15    http_client().get(&url).send().await?.text().await
16}
17
18/// Parses JSON metadata for the `asimov-modules` package and extracts module
19/// names from its runtime dependencies, removing the "asimov-" prefix and
20/// "-module" suffix.
21pub fn extract_module_names(json_str: impl AsRef<str>) -> serde_json::Result<Vec<ModuleMetadata>> {
22    let package: PackageMetadata = serde_json::from_str(json_str.as_ref())?;
23
24    // Extract the dependencies:
25    let Some(dependencies) = package.info.requires_dist else {
26        return Ok(Vec::new()); // no dependencies found
27    };
28
29    // Filter and transform the dependencies:
30    let module_names = dependencies
31        .into_iter()
32        .filter_map(|dep| {
33            // Extract the module name part (before any version specifiers):
34            let dep_name = dep
35                .split(|c| c == ' ' || c == '<' || c == '>' || c == ';' || c == '[')
36                .next()?;
37
38            // Handle the special case of "asimov-module" separately:
39            if dep_name == "asimov-module" {
40                return None;
41            }
42
43            // Check if the dependency name has the expected prefix and suffix:
44            if dep_name.starts_with("asimov-") && dep_name.ends_with("-module") {
45                // Strip the "asimov-" prefix and "-module" suffix:
46                let mod_name = dep_name.strip_prefix("asimov-")?.strip_suffix("-module")?;
47
48                Some(ModuleMetadata {
49                    name: mod_name.to_string(),
50                    version: package.info.version.clone(),
51                    r#type: ModuleType::Python,
52                    url: format!("https://pypi.org/project/{}/", dep_name),
53                })
54            } else {
55                None
56            }
57        })
58        .collect();
59
60    Ok(module_names)
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn test_extract_module_names() {
69        let json = r#"{
70            "info": {
71                "name": "asimov-modules",
72                "version": "25.0.0.dev0",
73                "requires_dist": [
74                    "asimov-mlx-module",
75                    "asimov-gpu-module>=1.0.0",
76                    "asimov-cpu-module; python_version >= '3.13'",
77                    "numpy>=1.20.0",
78                    "other-package"
79                ]
80            }
81        }"#;
82
83        let result: Vec<String> = extract_module_names(json)
84            .unwrap()
85            .iter()
86            .map(|m| m.name.clone())
87            .collect();
88        assert_eq!(result, vec!["mlx", "gpu", "cpu"]);
89    }
90
91    #[test]
92    fn test_no_dependencies() {
93        let json = r#"{
94            "info": {
95                "name": "asimov-modules",
96                "version": "25.0.0.dev0"
97            }
98        }"#;
99
100        let result = extract_module_names(json).unwrap();
101        assert!(result.is_empty());
102    }
103
104    #[test]
105    fn test_empty_dependencies() {
106        let json = r#"{
107            "info": {
108                "name": "asimov-modules",
109                "version": "25.0.0.dev0",
110                "requires_dist": []
111            }
112        }"#;
113
114        let result: Vec<String> = extract_module_names(json)
115            .unwrap()
116            .iter()
117            .map(|m| m.name.clone())
118            .collect();
119        assert!(result.is_empty());
120    }
121}