1use std::borrow::Cow;
2use std::fmt::{Display, Formatter};
3use std::ops::Deref;
4use std::path::Path;
5use std::str::FromStr;
6use std::sync::{Arc, LazyLock, RwLock};
7
8use http::StatusCode;
9use itertools::Either;
10use rustc_hash::{FxHashMap, FxHashSet};
11use thiserror::Error;
12use url::{ParseError, Url};
13use uv_auth::RealmRef;
14use uv_cache_key::CanonicalUrl;
15use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme};
16use uv_pypi_types::HashAlgorithm;
17use uv_redacted::DisplaySafeUrl;
18use uv_warnings::warn_user;
19
20use crate::{ExcludeNewerOverride, Index, IndexStatusCodeStrategy, Verbatim};
21
22pub static PYPI_URL: LazyLock<DisplaySafeUrl> =
23 LazyLock::new(|| DisplaySafeUrl::parse("https://pypi.org/simple").unwrap());
24
25static DEFAULT_INDEX: LazyLock<Index> = LazyLock::new(|| {
26 Index::from_index_url(IndexUrl::Pypi(Arc::new(VerbatimUrl::from_url(
27 PYPI_URL.clone(),
28 ))))
29});
30
31#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
33pub enum IndexUrl {
34 Pypi(Arc<VerbatimUrl>),
35 Url(Arc<VerbatimUrl>),
36 Path(Arc<VerbatimUrl>),
37}
38
39impl IndexUrl {
40 pub fn parse(path: &str, root_dir: Option<&Path>) -> Result<Self, IndexUrlError> {
45 let url = VerbatimUrl::from_url_or_path(path, root_dir)?;
46 Ok(Self::from(url))
47 }
48
49 pub fn root(&self) -> Option<DisplaySafeUrl> {
54 let mut segments = self.url().path_segments()?;
55 let last = match segments.next_back()? {
56 "" => segments.next_back()?,
58 segment => segment,
59 };
60
61 if !(last.eq_ignore_ascii_case("simple") || last.eq_ignore_ascii_case("+simple")) {
63 return None;
64 }
65
66 let mut url = self.url().clone();
67 url.path_segments_mut().ok()?.pop_if_empty().pop();
68 Some(url)
69 }
70}
71
72#[cfg(feature = "schemars")]
73impl schemars::JsonSchema for IndexUrl {
74 fn schema_name() -> Cow<'static, str> {
75 Cow::Borrowed("IndexUrl")
76 }
77
78 fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
79 schemars::json_schema!({
80 "type": "string",
81 "description": "The URL of an index to use for fetching packages (e.g., `https://pypi.org/simple`), or a local path."
82 })
83 }
84}
85
86impl IndexUrl {
87 #[inline]
88 fn inner(&self) -> &VerbatimUrl {
89 match self {
90 Self::Pypi(url) | Self::Url(url) | Self::Path(url) => url,
91 }
92 }
93
94 pub fn url(&self) -> &DisplaySafeUrl {
96 self.inner().raw()
97 }
98
99 pub fn into_url(self) -> DisplaySafeUrl {
101 match self {
102 Self::Pypi(url) | Self::Url(url) | Self::Path(url) => {
103 Arc::unwrap_or_clone(url).into_url()
104 }
105 }
106 }
107
108 pub fn without_credentials(&self) -> Cow<'_, DisplaySafeUrl> {
110 let url = self.url();
111 if url.username().is_empty() && url.password().is_none() {
112 Cow::Borrowed(url)
113 } else {
114 let mut url = url.clone();
115 let _ = url.set_username("");
116 let _ = url.set_password(None);
117 Cow::Owned(url)
118 }
119 }
120
121 pub fn warn_on_disambiguated_relative_path(&self) {
126 let Self::Path(verbatim_url) = &self else {
127 return;
128 };
129
130 if let Some(path) = verbatim_url.given()
131 && !is_disambiguated_path(path)
132 {
133 if cfg!(windows) {
134 warn_user!(
135 "Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `.\\{path}` or `./{path}`). Support for ambiguous values will be removed in the future"
136 );
137 } else {
138 warn_user!(
139 "Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `./{path}`). Support for ambiguous values will be removed in the future"
140 );
141 }
142 }
143 }
144}
145
146impl Display for IndexUrl {
147 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
148 Display::fmt(self.inner(), f)
149 }
150}
151
152impl Verbatim for IndexUrl {
153 fn verbatim(&self) -> Cow<'_, str> {
154 self.inner().verbatim()
155 }
156}
157
158fn is_disambiguated_path(path: &str) -> bool {
164 if cfg!(windows) {
165 if path.starts_with(".\\") || path.starts_with("..\\") || path.starts_with('/') {
166 return true;
167 }
168 }
169 if path.starts_with("./") || path.starts_with("../") || Path::new(path).is_absolute() {
170 return true;
171 }
172 if let Some((scheme, _)) = split_scheme(path) {
174 return Scheme::parse(scheme).is_some();
175 }
176 false
178}
179
180#[derive(Error, Debug)]
182pub enum IndexUrlError {
183 #[error(transparent)]
184 Io(#[from] std::io::Error),
185 #[error(transparent)]
186 Url(#[from] ParseError),
187 #[error(transparent)]
188 VerbatimUrl(#[from] VerbatimUrlError),
189}
190
191impl FromStr for IndexUrl {
192 type Err = IndexUrlError;
193
194 fn from_str(s: &str) -> Result<Self, Self::Err> {
195 Self::parse(s, None)
196 }
197}
198
199impl serde::ser::Serialize for IndexUrl {
200 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
201 where
202 S: serde::ser::Serializer,
203 {
204 self.inner().without_credentials().serialize(serializer)
205 }
206}
207
208impl<'de> serde::de::Deserialize<'de> for IndexUrl {
209 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
210 where
211 D: serde::de::Deserializer<'de>,
212 {
213 struct Visitor;
214
215 impl serde::de::Visitor<'_> for Visitor {
216 type Value = IndexUrl;
217
218 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
219 f.write_str("a string")
220 }
221
222 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
223 IndexUrl::from_str(v).map_err(serde::de::Error::custom)
224 }
225 }
226
227 deserializer.deserialize_str(Visitor)
228 }
229}
230
231impl From<VerbatimUrl> for IndexUrl {
232 fn from(url: VerbatimUrl) -> Self {
233 if url.scheme() == "file" {
234 Self::Path(Arc::new(url))
235 } else if *url.raw() == *PYPI_URL {
236 Self::Pypi(Arc::new(url))
237 } else {
238 Self::Url(Arc::new(url))
239 }
240 }
241}
242
243impl From<IndexUrl> for DisplaySafeUrl {
244 fn from(index: IndexUrl) -> Self {
245 index.into_url()
246 }
247}
248
249impl Deref for IndexUrl {
250 type Target = Url;
251
252 fn deref(&self) -> &Self::Target {
253 self.inner()
254 }
255}
256
257#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
262#[serde(rename_all = "kebab-case", deny_unknown_fields)]
263pub struct IndexLocations {
264 indexes: Vec<Index>,
265 flat_index: Vec<Index>,
266 no_index: bool,
267}
268
269impl IndexLocations {
270 pub fn new(indexes: Vec<Index>, flat_index: Vec<Index>, no_index: bool) -> Self {
272 Self {
273 indexes,
274 flat_index,
275 no_index,
276 }
277 }
278
279 #[must_use]
286 pub fn combine(self, indexes: Vec<Index>, flat_index: Vec<Index>, no_index: bool) -> Self {
287 Self {
288 indexes: self.indexes.into_iter().chain(indexes).collect(),
289 flat_index: self.flat_index.into_iter().chain(flat_index).collect(),
290 no_index: self.no_index || no_index,
291 }
292 }
293
294 pub fn is_none(&self) -> bool {
297 *self == Self::default()
298 }
299}
300
301fn is_same_index(a: &IndexUrl, b: &IndexUrl) -> bool {
303 RealmRef::from(&**b.url()) == RealmRef::from(&**a.url())
304 && CanonicalUrl::new(a.url().clone()) == CanonicalUrl::new(b.url().clone())
305}
306
307impl<'a> IndexLocations {
308 fn configured_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
310 let mut seen = FxHashSet::default();
311 self.indexes
312 .iter()
313 .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name)))
314 }
315
316 pub fn default_index(&'a self) -> Option<&'a Index> {
322 if self.no_index {
323 None
324 } else {
325 self.configured_indexes()
326 .find(|index| index.default)
327 .or_else(|| Some(&DEFAULT_INDEX))
328 }
329 }
330
331 pub fn implicit_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
335 if self.no_index {
336 Either::Left(std::iter::empty())
337 } else {
338 Either::Right(
339 self.configured_indexes()
340 .filter(|index| !index.default && !index.explicit),
341 )
342 }
343 }
344
345 pub fn explicit_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
349 if self.no_index {
350 Either::Left(std::iter::empty())
351 } else {
352 Either::Right(self.configured_indexes().filter(|index| index.explicit))
353 }
354 }
355
356 pub fn indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
365 self.implicit_indexes()
366 .chain(self.default_index())
367 .filter(|index| !index.explicit)
368 }
369
370 pub fn fetch_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
374 let mut seen = FxHashSet::default();
375 self.indexes()
376 .filter(move |index| seen.insert(index.raw_url()))
377 }
378
379 pub fn simple_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
383 if self.no_index {
384 Either::Left(std::iter::empty())
385 } else {
386 Either::Right(self.configured_indexes())
387 }
388 }
389
390 pub fn flat_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
392 self.flat_index.iter()
393 }
394
395 pub fn no_index(&self) -> bool {
397 self.no_index
398 }
399
400 pub fn allowed_indexes(&'a self) -> Vec<&'a Index> {
407 if self.no_index {
408 self.flat_index.iter().rev().collect()
409 } else {
410 let mut indexes = vec![];
411
412 let mut seen = FxHashSet::default();
413 let mut default = false;
414 for index in {
415 self.indexes
416 .iter()
417 .chain(self.flat_index.iter())
418 .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name)))
419 } {
420 if index.default {
421 if default {
422 continue;
423 }
424 default = true;
425 }
426 indexes.push(index);
427 }
428 if !default {
429 indexes.push(&*DEFAULT_INDEX);
430 }
431
432 indexes.reverse();
433 indexes
434 }
435 }
436
437 pub fn known_indexes(&'a self) -> impl Iterator<Item = &'a Index> {
446 if self.no_index {
447 Either::Left(self.flat_index.iter().rev())
448 } else {
449 Either::Right(
450 std::iter::once(&*DEFAULT_INDEX)
451 .chain(self.flat_index.iter().rev())
452 .chain(self.indexes.iter().rev()),
453 )
454 }
455 }
456
457 pub fn defined_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
467 if self.no_index {
468 return Either::Left(std::iter::empty());
469 }
470
471 let (non_default, default) = self
472 .configured_indexes()
473 .partition::<Vec<_>, _>(|index| !index.default);
474
475 Either::Right(non_default.into_iter().chain(default))
476 }
477
478 fn index_for_url(&self, url: &IndexUrl) -> Option<&Index> {
480 self.indexes
481 .iter()
482 .find(|index| is_same_index(index.url(), url))
483 }
484
485 pub fn status_code_strategy_for(&self, url: &IndexUrl) -> IndexStatusCodeStrategy {
487 self.index_for_url(url).map_or(
488 IndexStatusCodeStrategy::Default,
489 Index::status_code_strategy,
490 )
491 }
492
493 pub fn ignores_error_code_for(&self, url: &IndexUrl, status_code: StatusCode) -> bool {
495 self.index_for_url(url)
496 .is_some_and(|index| index.ignores_error_code(status_code))
497 }
498
499 pub fn simple_api_cache_control_for(&self, url: &IndexUrl) -> Option<http::HeaderValue> {
501 self.index_for_url(url)
502 .and_then(Index::simple_api_cache_control)
503 }
504
505 pub fn artifact_cache_control_for(&self, url: &IndexUrl) -> Option<http::HeaderValue> {
507 self.index_for_url(url)
508 .and_then(Index::artifact_cache_control)
509 }
510
511 pub fn hash_algorithm_for(&self, url: &IndexUrl) -> Option<HashAlgorithm> {
513 self.index_for_url(url)
514 .and_then(|index| index.hash_algorithm.map(HashAlgorithm::from))
515 }
516
517 pub fn exclude_newer_for(&self, url: &IndexUrl) -> Option<&ExcludeNewerOverride> {
519 self.index_for_url(url).and_then(Index::exclude_newer)
520 }
521}
522
523impl From<&IndexLocations> for uv_auth::Indexes {
524 fn from(index_locations: &IndexLocations) -> Self {
525 Self::from_indexes(index_locations.allowed_indexes().into_iter().map(|index| {
526 let mut url = index.url().url().clone();
527 url.set_username("").ok();
528 url.set_password(None).ok();
529 let mut root_url = index.url().root().unwrap_or_else(|| url.clone());
530 root_url.set_username("").ok();
531 root_url.set_password(None).ok();
532 uv_auth::Index {
533 url,
534 root_url,
535 auth_policy: index.authenticate,
536 }
537 }))
538 }
539}
540
541bitflags::bitflags! {
542 #[derive(Debug, Copy, Clone)]
543 struct Flags: u8 {
544 const NO_RANGE_REQUESTS = 1;
546 const UNAUTHORIZED = 1 << 2;
548 const FORBIDDEN = 1 << 1;
550 }
551}
552
553#[derive(Debug, Default, Clone)]
559pub struct IndexCapabilities(Arc<RwLock<FxHashMap<IndexUrl, Flags>>>);
560
561impl IndexCapabilities {
562 pub fn supports_range_requests(&self, index_url: &IndexUrl) -> bool {
564 !self
565 .0
566 .read()
567 .unwrap()
568 .get(index_url)
569 .is_some_and(|flags| flags.intersects(Flags::NO_RANGE_REQUESTS))
570 }
571
572 pub fn set_no_range_requests(&self, index_url: IndexUrl) {
574 self.0
575 .write()
576 .unwrap()
577 .entry(index_url)
578 .or_insert(Flags::empty())
579 .insert(Flags::NO_RANGE_REQUESTS);
580 }
581
582 pub fn unauthorized(&self, index_url: &IndexUrl) -> bool {
584 self.0
585 .read()
586 .unwrap()
587 .get(index_url)
588 .is_some_and(|flags| flags.intersects(Flags::UNAUTHORIZED))
589 }
590
591 pub(crate) fn set_unauthorized(&self, index_url: IndexUrl) {
593 self.0
594 .write()
595 .unwrap()
596 .entry(index_url)
597 .or_insert(Flags::empty())
598 .insert(Flags::UNAUTHORIZED);
599 }
600
601 pub fn forbidden(&self, index_url: &IndexUrl) -> bool {
603 self.0
604 .read()
605 .unwrap()
606 .get(index_url)
607 .is_some_and(|flags| flags.intersects(Flags::FORBIDDEN))
608 }
609
610 pub(crate) fn set_forbidden(&self, index_url: IndexUrl) {
612 self.0
613 .write()
614 .unwrap()
615 .entry(index_url)
616 .or_insert(Flags::empty())
617 .insert(Flags::FORBIDDEN);
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use std::error::Error;
624
625 use super::*;
626 use crate::{IndexCacheControl, IndexFormat, IndexName};
627 use http::HeaderValue;
628
629 fn index_urls<'a>(indexes: impl IntoIterator<Item = &'a Index>) -> Vec<&'a str> {
630 indexes
631 .into_iter()
632 .map(|index| index.url().url().as_str())
633 .collect()
634 }
635
636 #[test]
637 fn test_index_url_parse_valid_paths() {
638 assert!(is_disambiguated_path("/absolute/path"));
640 assert!(is_disambiguated_path("./relative/path"));
642 assert!(is_disambiguated_path("../../relative/path"));
643 if cfg!(windows) {
644 assert!(is_disambiguated_path("C:/absolute/path"));
646 assert!(is_disambiguated_path(".\\relative\\path"));
648 assert!(is_disambiguated_path("..\\..\\relative\\path"));
649 }
650 }
651
652 #[test]
653 fn test_index_url_parse_ambiguous_paths() {
654 assert!(!is_disambiguated_path("index"));
656 assert!(!is_disambiguated_path("relative/path"));
658 }
659
660 #[test]
661 fn test_index_url_parse_with_schemes() {
662 assert!(is_disambiguated_path("file:///absolute/path"));
663 assert!(is_disambiguated_path("https://registry.com/simple/"));
664 assert!(is_disambiguated_path(
665 "git+https://github.com/example/repo.git"
666 ));
667 }
668
669 #[test]
670 fn named_indexes_use_first_definition() -> Result<(), Box<dyn Error>> {
671 let first = Index::from_str("shared=https://first.example.com/simple")?;
672 let mut shadowed = Index::from_str("shared=https://shadowed.example.com/simple")?;
673 shadowed.explicit = true;
674 let mut explicit = Index::from_str("explicit=https://explicit.example.com/simple")?;
675 explicit.explicit = true;
676 let shadowed_implicit =
677 Index::from_str("explicit=https://shadowed-implicit.example.com/simple")?;
678 let mut default = Index::from_str("default=https://default.example.com/simple")?;
679 default.default = true;
680
681 let locations = IndexLocations::new(
682 vec![first, shadowed, explicit, shadowed_implicit, default],
683 vec![],
684 false,
685 );
686
687 assert_eq!(
688 index_urls(locations.simple_indexes()),
689 [
690 "https://first.example.com/simple",
691 "https://explicit.example.com/simple",
692 "https://default.example.com/simple",
693 ]
694 );
695 assert_eq!(
696 index_urls(locations.implicit_indexes()),
697 ["https://first.example.com/simple"]
698 );
699 assert_eq!(
700 index_urls(locations.explicit_indexes()),
701 ["https://explicit.example.com/simple"]
702 );
703 assert_eq!(
704 index_urls(locations.indexes()),
705 [
706 "https://first.example.com/simple",
707 "https://default.example.com/simple",
708 ]
709 );
710 assert_eq!(
711 index_urls(locations.defined_indexes()),
712 [
713 "https://first.example.com/simple",
714 "https://explicit.example.com/simple",
715 "https://default.example.com/simple",
716 ]
717 );
718
719 Ok(())
720 }
721
722 #[test]
723 fn unnamed_indexes_are_not_deduplicated() -> Result<(), Box<dyn Error>> {
724 let first = Index::from_str("https://first.example.com/simple")?;
725 let repeated = Index::from_str("https://first.example.com/simple")?;
726 let last = Index::from_str("https://last.example.com/simple")?;
727 let locations = IndexLocations::new(vec![first, repeated, last], vec![], false);
728 let expected = [
729 "https://first.example.com/simple",
730 "https://first.example.com/simple",
731 "https://last.example.com/simple",
732 ];
733
734 assert_eq!(index_urls(locations.simple_indexes()), expected);
735 assert_eq!(index_urls(locations.implicit_indexes()), expected);
736 assert_eq!(index_urls(locations.defined_indexes()), expected);
737 assert_eq!(
738 index_urls(locations.fetch_indexes()),
739 [
740 "https://first.example.com/simple",
741 "https://last.example.com/simple",
742 "https://pypi.org/simple",
743 ]
744 );
745
746 Ok(())
747 }
748
749 #[test]
750 fn shadowed_default_falls_back_to_pypi() -> Result<(), Box<dyn Error>> {
751 let first = Index::from_str("shared=https://first.example.com/simple")?;
752 let mut shadowed = Index::from_str("shared=https://shadowed.example.com/simple")?;
753 shadowed.default = true;
754 let locations = IndexLocations::new(vec![first, shadowed], vec![], false);
755
756 assert_eq!(
757 index_urls(locations.default_index()),
758 ["https://pypi.org/simple"]
759 );
760 assert_eq!(
761 index_urls(locations.indexes()),
762 [
763 "https://first.example.com/simple",
764 "https://pypi.org/simple",
765 ]
766 );
767
768 Ok(())
769 }
770
771 #[test]
772 fn fetch_indexes_deduplicates_raw_urls() {
773 let url = IndexUrl::from_str("https://index.example.com/simple").unwrap();
774 let mut first = Index::from(url.clone());
775 first.name = Some(IndexName::from_str("first").unwrap());
776 let mut second = Index::from(url);
777 second.name = Some(IndexName::from_str("second").unwrap());
778 second.default = true;
779 let locations = IndexLocations::new(vec![first, second], Vec::new(), false);
780
781 assert_eq!(locations.indexes().count(), 2);
782 assert_eq!(locations.fetch_indexes().count(), 1);
783 }
784
785 #[test]
786 fn test_cache_control_lookup() {
787 use std::str::FromStr;
788
789 use crate::IndexFormat;
790 use crate::index_name::IndexName;
791
792 let indexes = vec![
793 Index {
794 name: Some(IndexName::from_str("index1").unwrap()),
795 url: IndexUrl::from_str("https://index1.example.com/simple").unwrap(),
796 cache_control: Some(crate::IndexCacheControl {
797 api: Some(HeaderValue::from_static("max-age=300")),
798 files: Some(HeaderValue::from_static("max-age=1800")),
799 }),
800 explicit: false,
801 default: false,
802 origin: None,
803 format: IndexFormat::Simple,
804 publish_url: None,
805 authenticate: uv_auth::AuthPolicy::default(),
806 ignore_error_codes: None,
807 hash_algorithm: None,
808 exclude_newer: None,
809 },
810 Index {
811 name: Some(IndexName::from_str("index2").unwrap()),
812 url: IndexUrl::from_str("https://index2.example.com/simple").unwrap(),
813 cache_control: None,
814 explicit: false,
815 default: false,
816 origin: None,
817 format: IndexFormat::Simple,
818 publish_url: None,
819 authenticate: uv_auth::AuthPolicy::default(),
820 ignore_error_codes: None,
821 hash_algorithm: None,
822 exclude_newer: None,
823 },
824 ];
825
826 let index_locations = IndexLocations::new(indexes, Vec::new(), false);
827
828 let url1 = IndexUrl::from_str("https://index1.example.com/simple").unwrap();
829 assert_eq!(
830 index_locations.simple_api_cache_control_for(&url1),
831 Some(HeaderValue::from_static("max-age=300"))
832 );
833 assert_eq!(
834 index_locations.artifact_cache_control_for(&url1),
835 Some(HeaderValue::from_static("max-age=1800"))
836 );
837
838 let url2 = IndexUrl::from_str("https://index2.example.com/simple").unwrap();
839 assert_eq!(index_locations.simple_api_cache_control_for(&url2), None);
840 assert_eq!(index_locations.artifact_cache_control_for(&url2), None);
841
842 let url3 = IndexUrl::from_str("https://index3.example.com/simple").unwrap();
843 assert_eq!(index_locations.simple_api_cache_control_for(&url3), None);
844 assert_eq!(index_locations.artifact_cache_control_for(&url3), None);
845 }
846
847 #[test]
848 fn test_pytorch_default_cache_control() {
849 let indexes = vec![Index {
851 name: Some(IndexName::from_str("pytorch").unwrap()),
852 url: IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(),
853 cache_control: None, explicit: false,
855 default: false,
856 origin: None,
857 format: IndexFormat::Simple,
858 publish_url: None,
859 authenticate: uv_auth::AuthPolicy::default(),
860 ignore_error_codes: None,
861 hash_algorithm: None,
862 exclude_newer: None,
863 }];
864
865 let index_locations = IndexLocations::new(indexes, Vec::new(), false);
866
867 let pytorch_url = IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap();
868
869 assert_eq!(
870 index_locations.simple_api_cache_control_for(&pytorch_url),
871 None
872 );
873 assert_eq!(
874 index_locations.artifact_cache_control_for(&pytorch_url),
875 Some(HeaderValue::from_static(
876 "max-age=365000000, immutable, public",
877 ))
878 );
879 }
880
881 #[test]
882 fn test_pytorch_user_override_cache_control() {
883 let indexes = vec![Index {
885 name: Some(IndexName::from_str("pytorch").unwrap()),
886 url: IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(),
887 cache_control: Some(IndexCacheControl {
888 api: Some(HeaderValue::from_static("no-cache")),
889 files: Some(HeaderValue::from_static("max-age=3600")),
890 }),
891 explicit: false,
892 default: false,
893 origin: None,
894 format: IndexFormat::Simple,
895 publish_url: None,
896 authenticate: uv_auth::AuthPolicy::default(),
897 ignore_error_codes: None,
898 hash_algorithm: None,
899 exclude_newer: None,
900 }];
901
902 let index_locations = IndexLocations::new(indexes, Vec::new(), false);
903
904 let pytorch_url = IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap();
905
906 assert_eq!(
907 index_locations.simple_api_cache_control_for(&pytorch_url),
908 Some(HeaderValue::from_static("no-cache"))
909 );
910 assert_eq!(
911 index_locations.artifact_cache_control_for(&pytorch_url),
912 Some(HeaderValue::from_static("max-age=3600"))
913 );
914 }
915
916 #[test]
917 fn test_nvidia_default_cache_control() {
918 let indexes = vec![Index {
920 name: Some(IndexName::from_str("nvidia").unwrap()),
921 url: IndexUrl::from_str("https://pypi.nvidia.com").unwrap(),
922 cache_control: None, explicit: false,
924 default: false,
925 origin: None,
926 format: IndexFormat::Simple,
927 publish_url: None,
928 authenticate: uv_auth::AuthPolicy::default(),
929 ignore_error_codes: None,
930 hash_algorithm: None,
931 exclude_newer: None,
932 }];
933
934 let index_locations = IndexLocations::new(indexes, Vec::new(), false);
935
936 let nvidia_url = IndexUrl::from_str("https://pypi.nvidia.com").unwrap();
937
938 assert_eq!(
939 index_locations.simple_api_cache_control_for(&nvidia_url),
940 None
941 );
942 assert_eq!(
943 index_locations.artifact_cache_control_for(&nvidia_url),
944 Some(HeaderValue::from_static(
945 "max-age=365000000, immutable, public",
946 ))
947 );
948 }
949}