use crate::secrets::ProtectedString;
use serde::{Deserialize, Serialize};
use std::env;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PypiConfig {
#[serde(default = "super::super::default_true")]
pub enabled: bool,
#[serde(default = "default_pypi_proxy")]
pub proxy: Option<String>,
#[serde(default, skip_serializing)]
pub proxy_auth: Option<ProtectedString>,
#[serde(default)]
pub proxies: Vec<PypiProxyEntry>,
#[serde(default = "super::super::default_timeout")]
pub proxy_timeout: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PypiProxyEntry {
Simple(String),
Full(PypiProxy),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PypiProxy {
pub url: String,
#[serde(default, skip_serializing)]
pub auth: Option<ProtectedString>,
}
impl PypiProxyEntry {
pub fn url(&self) -> &str {
match self {
PypiProxyEntry::Simple(s) => s,
PypiProxyEntry::Full(p) => &p.url,
}
}
pub fn auth(&self) -> Option<&str> {
use crate::secrets::expose_opt;
match self {
PypiProxyEntry::Simple(_) => None,
PypiProxyEntry::Full(p) => expose_opt(&p.auth),
}
}
}
fn default_pypi_proxy() -> Option<String> {
Some("https://pypi.org/simple/".to_string())
}
impl Default for PypiConfig {
fn default() -> Self {
Self {
enabled: true,
proxy: default_pypi_proxy(),
proxy_auth: None,
proxies: Vec::new(),
proxy_timeout: 30,
}
}
}
impl PypiConfig {
pub fn upstreams(&self) -> Vec<PypiProxyEntry> {
if !self.proxies.is_empty() {
self.proxies.clone()
} else if let Some(url) = &self.proxy {
vec![match &self.proxy_auth {
Some(auth) => PypiProxyEntry::Full(PypiProxy {
url: url.clone(),
auth: Some(auth.clone()),
}),
None => PypiProxyEntry::Simple(url.clone()),
}]
} else {
Vec::new()
}
}
pub(in crate::config) fn apply_env_overrides(&mut self) {
if let Ok(val) = env::var("NORA_PYPI_ENABLED") {
self.enabled = val.to_lowercase() == "true" || val == "1";
}
if let Ok(val) = env::var("NORA_PYPI_PROXY") {
self.proxy = if val.is_empty() { None } else { Some(val) };
}
if let Ok(val) = env::var("NORA_PYPI_PROXY_AUTH") {
self.proxy_auth = if val.is_empty() {
None
} else {
Some(ProtectedString::new(val))
};
}
if let Ok(val) = env::var("NORA_PYPI_PROXIES") {
self.proxies = val
.split(',')
.filter(|s| !s.trim().is_empty())
.map(|s| {
let parts: Vec<&str> = s.trim().splitn(2, '|').collect();
if parts.len() > 1 {
PypiProxyEntry::Full(PypiProxy {
url: parts[0].to_string(),
auth: Some(ProtectedString::from(parts[1])),
})
} else {
PypiProxyEntry::Simple(parts[0].to_string())
}
})
.collect();
}
if let Ok(val) = env::var("NORA_PYPI_PROXY_TIMEOUT") {
super::super::parse_env_warn("NORA_PYPI_PROXY_TIMEOUT", &val, &mut self.proxy_timeout);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn upstreams_prefers_proxies_then_legacy_then_empty() {
let c = PypiConfig::default();
let u = c.upstreams();
assert_eq!(u.len(), 1);
assert_eq!(u[0].url(), "https://pypi.org/simple/");
let mut c = PypiConfig::default();
c.proxy_auth = Some(ProtectedString::new("tok".into()));
assert_eq!(c.upstreams()[0].auth(), Some("tok"));
let mut c = PypiConfig::default();
c.proxies = vec![
PypiProxyEntry::Simple("https://a/simple".into()),
PypiProxyEntry::Simple("https://b/simple".into()),
];
let u = c.upstreams();
assert_eq!(u.len(), 2);
assert_eq!(u[0].url(), "https://a/simple");
assert_eq!(u[1].url(), "https://b/simple");
let mut c = PypiConfig::default();
c.proxy = None;
assert!(c.upstreams().is_empty());
}
#[test]
fn env_proxies_parse_url_and_optional_auth() {
let mut c = PypiConfig::default();
let val = "https://pypi.org/simple,https://download.pytorch.org/whl/cu124|sometoken";
c.proxies = val
.split(',')
.filter(|s| !s.trim().is_empty())
.map(|s| {
let parts: Vec<&str> = s.trim().splitn(2, '|').collect();
if parts.len() > 1 {
PypiProxyEntry::Full(PypiProxy {
url: parts[0].to_string(),
auth: Some(ProtectedString::from(parts[1])),
})
} else {
PypiProxyEntry::Simple(parts[0].to_string())
}
})
.collect();
assert_eq!(c.proxies.len(), 2);
assert_eq!(c.proxies[0].url(), "https://pypi.org/simple");
assert_eq!(c.proxies[0].auth(), None);
assert_eq!(c.proxies[1].url(), "https://download.pytorch.org/whl/cu124");
assert_eq!(c.proxies[1].auth(), Some("sometoken"));
}
}