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 itertools::Either;
9use rustc_hash::{FxHashMap, FxHashSet};
10use thiserror::Error;
11use url::{ParseError, Url};
12use uv_auth::RealmRef;
13use uv_cache_key::CanonicalUrl;
14use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme};
15use uv_redacted::DisplaySafeUrl;
16use uv_warnings::warn_user;
17
18use crate::{ExcludeNewerOverride, Index, IndexStatusCodeStrategy, Verbatim};
19
20pub static PYPI_URL: LazyLock<DisplaySafeUrl> =
21 LazyLock::new(|| DisplaySafeUrl::parse("https://pypi.org/simple").unwrap());
22
23static DEFAULT_INDEX: LazyLock<Index> = LazyLock::new(|| {
24 Index::from_index_url(IndexUrl::Pypi(Arc::new(VerbatimUrl::from_url(
25 PYPI_URL.clone(),
26 ))))
27});
28
29#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
31pub enum IndexUrl {
32 Pypi(Arc<VerbatimUrl>),
33 Url(Arc<VerbatimUrl>),
34 Path(Arc<VerbatimUrl>),
35}
36
37impl IndexUrl {
38 pub fn parse(path: &str, root_dir: Option<&Path>) -> Result<Self, IndexUrlError> {
43 let url = VerbatimUrl::from_url_or_path(path, root_dir)?;
44 Ok(Self::from(url))
45 }
46
47 pub fn root(&self) -> Option<DisplaySafeUrl> {
52 let mut segments = self.url().path_segments()?;
53 let last = match segments.next_back()? {
54 "" => segments.next_back()?,
56 segment => segment,
57 };
58
59 if !(last.eq_ignore_ascii_case("simple") || last.eq_ignore_ascii_case("+simple")) {
61 return None;
62 }
63
64 let mut url = self.url().clone();
65 url.path_segments_mut().ok()?.pop_if_empty().pop();
66 Some(url)
67 }
68}
69
70#[cfg(feature = "schemars")]
71impl schemars::JsonSchema for IndexUrl {
72 fn schema_name() -> Cow<'static, str> {
73 Cow::Borrowed("IndexUrl")
74 }
75
76 fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
77 schemars::json_schema!({
78 "type": "string",
79 "description": "The URL of an index to use for fetching packages (e.g., `https://pypi.org/simple`), or a local path."
80 })
81 }
82}
83
84impl IndexUrl {
85 #[inline]
86 fn inner(&self) -> &VerbatimUrl {
87 match self {
88 Self::Pypi(url) | Self::Url(url) | Self::Path(url) => url,
89 }
90 }
91
92 pub fn url(&self) -> &DisplaySafeUrl {
94 self.inner().raw()
95 }
96
97 pub fn into_url(self) -> DisplaySafeUrl {
99 self.inner().to_url()
100 }
101
102 pub fn without_credentials(&self) -> Cow<'_, DisplaySafeUrl> {
104 let url = self.url();
105 if url.username().is_empty() && url.password().is_none() {
106 Cow::Borrowed(url)
107 } else {
108 let mut url = url.clone();
109 let _ = url.set_username("");
110 let _ = url.set_password(None);
111 Cow::Owned(url)
112 }
113 }
114
115 pub fn warn_on_disambiguated_relative_path(&self) {
120 let Self::Path(verbatim_url) = &self else {
121 return;
122 };
123
124 if let Some(path) = verbatim_url.given() {
125 if !is_disambiguated_path(path) {
126 if cfg!(windows) {
127 warn_user!(
128 "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"
129 );
130 } else {
131 warn_user!(
132 "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"
133 );
134 }
135 }
136 }
137 }
138}
139
140impl Display for IndexUrl {
141 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
142 Display::fmt(self.inner(), f)
143 }
144}
145
146impl Verbatim for IndexUrl {
147 fn verbatim(&self) -> Cow<'_, str> {
148 self.inner().verbatim()
149 }
150}
151
152fn is_disambiguated_path(path: &str) -> bool {
158 if cfg!(windows) {
159 if path.starts_with(".\\") || path.starts_with("..\\") || path.starts_with('/') {
160 return true;
161 }
162 }
163 if path.starts_with("./") || path.starts_with("../") || Path::new(path).is_absolute() {
164 return true;
165 }
166 if let Some((scheme, _)) = split_scheme(path) {
168 return Scheme::parse(scheme).is_some();
169 }
170 false
172}
173
174#[derive(Error, Debug)]
176pub enum IndexUrlError {
177 #[error(transparent)]
178 Io(#[from] std::io::Error),
179 #[error(transparent)]
180 Url(#[from] ParseError),
181 #[error(transparent)]
182 VerbatimUrl(#[from] VerbatimUrlError),
183}
184
185impl FromStr for IndexUrl {
186 type Err = IndexUrlError;
187
188 fn from_str(s: &str) -> Result<Self, Self::Err> {
189 Self::parse(s, None)
190 }
191}
192
193impl serde::ser::Serialize for IndexUrl {
194 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
195 where
196 S: serde::ser::Serializer,
197 {
198 self.inner().without_credentials().serialize(serializer)
199 }
200}
201
202impl<'de> serde::de::Deserialize<'de> for IndexUrl {
203 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
204 where
205 D: serde::de::Deserializer<'de>,
206 {
207 struct Visitor;
208
209 impl serde::de::Visitor<'_> for Visitor {
210 type Value = IndexUrl;
211
212 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
213 f.write_str("a string")
214 }
215
216 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
217 IndexUrl::from_str(v).map_err(serde::de::Error::custom)
218 }
219 }
220
221 deserializer.deserialize_str(Visitor)
222 }
223}
224
225impl From<VerbatimUrl> for IndexUrl {
226 fn from(url: VerbatimUrl) -> Self {
227 if url.scheme() == "file" {
228 Self::Path(Arc::new(url))
229 } else if *url.raw() == *PYPI_URL {
230 Self::Pypi(Arc::new(url))
231 } else {
232 Self::Url(Arc::new(url))
233 }
234 }
235}
236
237impl From<IndexUrl> for DisplaySafeUrl {
238 fn from(index: IndexUrl) -> Self {
239 index.inner().to_url()
240 }
241}
242
243impl Deref for IndexUrl {
244 type Target = Url;
245
246 fn deref(&self) -> &Self::Target {
247 self.inner()
248 }
249}
250
251#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
256#[serde(rename_all = "kebab-case", deny_unknown_fields)]
257pub struct IndexLocations {
258 indexes: Vec<Index>,
259 flat_index: Vec<Index>,
260 no_index: bool,
261}
262
263impl IndexLocations {
264 pub fn new(indexes: Vec<Index>, flat_index: Vec<Index>, no_index: bool) -> Self {
266 Self {
267 indexes,
268 flat_index,
269 no_index,
270 }
271 }
272
273 #[must_use]
280 pub fn combine(self, indexes: Vec<Index>, flat_index: Vec<Index>, no_index: bool) -> Self {
281 Self {
282 indexes: self.indexes.into_iter().chain(indexes).collect(),
283 flat_index: self.flat_index.into_iter().chain(flat_index).collect(),
284 no_index: self.no_index || no_index,
285 }
286 }
287
288 pub fn is_none(&self) -> bool {
291 *self == Self::default()
292 }
293}
294
295fn is_same_index(a: &IndexUrl, b: &IndexUrl) -> bool {
297 RealmRef::from(&**b.url()) == RealmRef::from(&**a.url())
298 && CanonicalUrl::new(a.url()) == CanonicalUrl::new(b.url())
299}
300
301impl<'a> IndexLocations {
302 pub fn default_index(&'a self) -> Option<&'a Index> {
308 if self.no_index {
309 None
310 } else {
311 let mut seen = FxHashSet::default();
312 self.indexes
313 .iter()
314 .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name)))
315 .find(|index| index.default)
316 .or_else(|| Some(&DEFAULT_INDEX))
317 }
318 }
319
320 pub fn implicit_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
324 if self.no_index {
325 Either::Left(std::iter::empty())
326 } else {
327 let mut seen = FxHashSet::default();
328 Either::Right(
329 self.indexes
330 .iter()
331 .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name)))
332 .filter(|index| !index.default && !index.explicit),
333 )
334 }
335 }
336
337 pub fn explicit_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
341 if self.no_index {
342 Either::Left(std::iter::empty())
343 } else {
344 let mut seen = FxHashSet::default();
345 Either::Right(
346 self.indexes
347 .iter()
348 .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name)))
349 .filter(|index| index.explicit),
350 )
351 }
352 }
353
354 pub fn indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
363 self.implicit_indexes()
364 .chain(self.default_index())
365 .filter(|index| !index.explicit)
366 }
367
368 pub fn simple_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
372 if self.no_index {
373 Either::Left(std::iter::empty())
374 } else {
375 let mut seen = FxHashSet::default();
376 Either::Right(
377 self.indexes
378 .iter()
379 .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name))),
380 )
381 }
382 }
383
384 pub fn flat_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
386 self.flat_index.iter()
387 }
388
389 pub fn no_index(&self) -> bool {
391 self.no_index
392 }
393
394 pub fn index_urls(&'a self) -> IndexUrls {
396 IndexUrls {
397 indexes: self.indexes.clone(),
398 flat_indexes: self.flat_index.clone(),
399 no_index: self.no_index,
400 }
401 }
402
403 pub fn allowed_indexes(&'a self) -> Vec<&'a Index> {
410 if self.no_index {
411 self.flat_index.iter().rev().collect()
412 } else {
413 let mut indexes = vec![];
414
415 let mut seen = FxHashSet::default();
416 let mut default = false;
417 for index in {
418 self.indexes
419 .iter()
420 .chain(self.flat_index.iter())
421 .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name)))
422 } {
423 if index.default {
424 if default {
425 continue;
426 }
427 default = true;
428 }
429 indexes.push(index);
430 }
431 if !default {
432 indexes.push(&*DEFAULT_INDEX);
433 }
434
435 indexes.reverse();
436 indexes
437 }
438 }
439
440 pub fn known_indexes(&'a self) -> impl Iterator<Item = &'a Index> {
449 if self.no_index {
450 Either::Left(self.flat_index.iter().rev())
451 } else {
452 Either::Right(
453 std::iter::once(&*DEFAULT_INDEX)
454 .chain(self.flat_index.iter().rev())
455 .chain(self.indexes.iter().rev()),
456 )
457 }
458 }
459
460 pub fn simple_api_cache_control_for(&self, url: &IndexUrl) -> Option<http::HeaderValue> {
462 for index in &self.indexes {
463 if is_same_index(index.url(), url) {
464 return index.simple_api_cache_control();
465 }
466 }
467 None
468 }
469
470 pub fn artifact_cache_control_for(&self, url: &IndexUrl) -> Option<http::HeaderValue> {
472 for index in &self.indexes {
473 if is_same_index(index.url(), url) {
474 return index.artifact_cache_control();
475 }
476 }
477 None
478 }
479
480 pub fn exclude_newer_for(&self, url: &IndexUrl) -> Option<&ExcludeNewerOverride> {
482 for index in &self.indexes {
483 if is_same_index(index.url(), url) {
484 return index.exclude_newer();
485 }
486 }
487 None
488 }
489}
490
491impl From<&IndexLocations> for uv_auth::Indexes {
492 fn from(index_locations: &IndexLocations) -> Self {
493 Self::from_indexes(index_locations.allowed_indexes().into_iter().map(|index| {
494 let mut url = index.url().url().clone();
495 url.set_username("").ok();
496 url.set_password(None).ok();
497 let mut root_url = index.url().root().unwrap_or_else(|| url.clone());
498 root_url.set_username("").ok();
499 root_url.set_password(None).ok();
500 uv_auth::Index {
501 url,
502 root_url,
503 auth_policy: index.authenticate,
504 }
505 }))
506 }
507}
508
509#[derive(Default, Debug, Clone, PartialEq, Eq)]
514pub struct IndexUrls {
515 indexes: Vec<Index>,
516 flat_indexes: Vec<Index>,
517 no_index: bool,
518}
519
520impl<'a> IndexUrls {
521 pub fn from_indexes(indexes: Vec<Index>) -> Self {
522 Self {
523 indexes,
524 flat_indexes: Vec::new(),
525 no_index: false,
526 }
527 }
528
529 pub fn flat_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
531 self.flat_indexes.iter()
532 }
533
534 fn default_index(&'a self) -> Option<&'a Index> {
540 if self.no_index {
541 None
542 } else {
543 let mut seen = FxHashSet::default();
544 self.indexes
545 .iter()
546 .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name)))
547 .find(|index| index.default)
548 .or_else(|| Some(&DEFAULT_INDEX))
549 }
550 }
551
552 fn implicit_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
556 if self.no_index {
557 Either::Left(std::iter::empty())
558 } else {
559 let mut seen = FxHashSet::default();
560 Either::Right(
561 self.indexes
562 .iter()
563 .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name)))
564 .filter(|index| !index.default && !index.explicit),
565 )
566 }
567 }
568
569 pub fn indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
577 let mut seen = FxHashSet::default();
578 self.implicit_indexes()
579 .chain(self.default_index())
580 .filter(|index| !index.explicit)
581 .filter(move |index| seen.insert(index.raw_url())) }
583
584 pub fn defined_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
595 if self.no_index {
596 return Either::Left(std::iter::empty());
597 }
598
599 let mut seen = FxHashSet::default();
600 let (non_default, default) = self
601 .indexes
602 .iter()
603 .filter(move |index| {
604 if let Some(name) = &index.name {
605 seen.insert(name)
606 } else {
607 true
608 }
609 })
610 .partition::<Vec<_>, _>(|index| !index.default);
611
612 Either::Right(non_default.into_iter().chain(default))
613 }
614
615 pub fn no_index(&self) -> bool {
617 self.no_index
618 }
619
620 pub fn status_code_strategy_for(&self, url: &IndexUrl) -> IndexStatusCodeStrategy {
622 for index in &self.indexes {
623 if is_same_index(index.url(), url) {
624 return index.status_code_strategy();
625 }
626 }
627 IndexStatusCodeStrategy::Default
628 }
629
630 pub fn simple_api_cache_control_for(&self, url: &IndexUrl) -> Option<http::HeaderValue> {
632 for index in &self.indexes {
633 if is_same_index(index.url(), url) {
634 return index.simple_api_cache_control();
635 }
636 }
637 None
638 }
639
640 pub fn artifact_cache_control_for(&self, url: &IndexUrl) -> Option<http::HeaderValue> {
642 for index in &self.indexes {
643 if is_same_index(index.url(), url) {
644 return index.artifact_cache_control();
645 }
646 }
647 None
648 }
649}
650
651bitflags::bitflags! {
652 #[derive(Debug, Copy, Clone)]
653 struct Flags: u8 {
654 const NO_RANGE_REQUESTS = 1;
656 const UNAUTHORIZED = 1 << 2;
658 const FORBIDDEN = 1 << 1;
660 }
661}
662
663#[derive(Debug, Default, Clone)]
669pub struct IndexCapabilities(Arc<RwLock<FxHashMap<IndexUrl, Flags>>>);
670
671impl IndexCapabilities {
672 pub fn supports_range_requests(&self, index_url: &IndexUrl) -> bool {
674 !self
675 .0
676 .read()
677 .unwrap()
678 .get(index_url)
679 .is_some_and(|flags| flags.intersects(Flags::NO_RANGE_REQUESTS))
680 }
681
682 pub fn set_no_range_requests(&self, index_url: IndexUrl) {
684 self.0
685 .write()
686 .unwrap()
687 .entry(index_url)
688 .or_insert(Flags::empty())
689 .insert(Flags::NO_RANGE_REQUESTS);
690 }
691
692 pub fn unauthorized(&self, index_url: &IndexUrl) -> bool {
694 self.0
695 .read()
696 .unwrap()
697 .get(index_url)
698 .is_some_and(|flags| flags.intersects(Flags::UNAUTHORIZED))
699 }
700
701 pub(crate) fn set_unauthorized(&self, index_url: IndexUrl) {
703 self.0
704 .write()
705 .unwrap()
706 .entry(index_url)
707 .or_insert(Flags::empty())
708 .insert(Flags::UNAUTHORIZED);
709 }
710
711 pub fn forbidden(&self, index_url: &IndexUrl) -> bool {
713 self.0
714 .read()
715 .unwrap()
716 .get(index_url)
717 .is_some_and(|flags| flags.intersects(Flags::FORBIDDEN))
718 }
719
720 pub(crate) fn set_forbidden(&self, index_url: IndexUrl) {
722 self.0
723 .write()
724 .unwrap()
725 .entry(index_url)
726 .or_insert(Flags::empty())
727 .insert(Flags::FORBIDDEN);
728 }
729}
730
731#[cfg(test)]
732mod tests {
733 use super::*;
734 use crate::{IndexCacheControl, IndexFormat, IndexName};
735 use http::HeaderValue;
736
737 #[test]
738 fn test_index_url_parse_valid_paths() {
739 assert!(is_disambiguated_path("/absolute/path"));
741 assert!(is_disambiguated_path("./relative/path"));
743 assert!(is_disambiguated_path("../../relative/path"));
744 if cfg!(windows) {
745 assert!(is_disambiguated_path("C:/absolute/path"));
747 assert!(is_disambiguated_path(".\\relative\\path"));
749 assert!(is_disambiguated_path("..\\..\\relative\\path"));
750 }
751 }
752
753 #[test]
754 fn test_index_url_parse_ambiguous_paths() {
755 assert!(!is_disambiguated_path("index"));
757 assert!(!is_disambiguated_path("relative/path"));
759 }
760
761 #[test]
762 fn test_index_url_parse_with_schemes() {
763 assert!(is_disambiguated_path("file:///absolute/path"));
764 assert!(is_disambiguated_path("https://registry.com/simple/"));
765 assert!(is_disambiguated_path(
766 "git+https://github.com/example/repo.git"
767 ));
768 }
769
770 #[test]
771 fn test_cache_control_lookup() {
772 use std::str::FromStr;
773
774 use crate::IndexFormat;
775 use crate::index_name::IndexName;
776
777 let indexes = vec![
778 Index {
779 name: Some(IndexName::from_str("index1").unwrap()),
780 url: IndexUrl::from_str("https://index1.example.com/simple").unwrap(),
781 cache_control: Some(crate::IndexCacheControl {
782 api: Some(HeaderValue::from_static("max-age=300")),
783 files: Some(HeaderValue::from_static("max-age=1800")),
784 }),
785 explicit: false,
786 default: false,
787 origin: None,
788 format: IndexFormat::Simple,
789 publish_url: None,
790 authenticate: uv_auth::AuthPolicy::default(),
791 ignore_error_codes: None,
792 exclude_newer: None,
793 },
794 Index {
795 name: Some(IndexName::from_str("index2").unwrap()),
796 url: IndexUrl::from_str("https://index2.example.com/simple").unwrap(),
797 cache_control: None,
798 explicit: false,
799 default: false,
800 origin: None,
801 format: IndexFormat::Simple,
802 publish_url: None,
803 authenticate: uv_auth::AuthPolicy::default(),
804 ignore_error_codes: None,
805 exclude_newer: None,
806 },
807 ];
808
809 let index_urls = IndexUrls::from_indexes(indexes);
810
811 let url1 = IndexUrl::from_str("https://index1.example.com/simple").unwrap();
812 assert_eq!(
813 index_urls.simple_api_cache_control_for(&url1),
814 Some(HeaderValue::from_static("max-age=300"))
815 );
816 assert_eq!(
817 index_urls.artifact_cache_control_for(&url1),
818 Some(HeaderValue::from_static("max-age=1800"))
819 );
820
821 let url2 = IndexUrl::from_str("https://index2.example.com/simple").unwrap();
822 assert_eq!(index_urls.simple_api_cache_control_for(&url2), None);
823 assert_eq!(index_urls.artifact_cache_control_for(&url2), None);
824
825 let url3 = IndexUrl::from_str("https://index3.example.com/simple").unwrap();
826 assert_eq!(index_urls.simple_api_cache_control_for(&url3), None);
827 assert_eq!(index_urls.artifact_cache_control_for(&url3), None);
828 }
829
830 #[test]
831 fn test_pytorch_default_cache_control() {
832 let indexes = vec![Index {
834 name: Some(IndexName::from_str("pytorch").unwrap()),
835 url: IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(),
836 cache_control: None, explicit: false,
838 default: false,
839 origin: None,
840 format: IndexFormat::Simple,
841 publish_url: None,
842 authenticate: uv_auth::AuthPolicy::default(),
843 ignore_error_codes: None,
844 exclude_newer: None,
845 }];
846
847 let index_urls = IndexUrls::from_indexes(indexes.clone());
848 let index_locations = IndexLocations::new(indexes, Vec::new(), false);
849
850 let pytorch_url = IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap();
851
852 assert_eq!(index_urls.simple_api_cache_control_for(&pytorch_url), None);
854 assert_eq!(
855 index_urls.artifact_cache_control_for(&pytorch_url),
856 Some(HeaderValue::from_static(
857 "max-age=365000000, immutable, public",
858 ))
859 );
860
861 assert_eq!(
863 index_locations.simple_api_cache_control_for(&pytorch_url),
864 None
865 );
866 assert_eq!(
867 index_locations.artifact_cache_control_for(&pytorch_url),
868 Some(HeaderValue::from_static(
869 "max-age=365000000, immutable, public",
870 ))
871 );
872 }
873
874 #[test]
875 fn test_pytorch_user_override_cache_control() {
876 let indexes = vec![Index {
878 name: Some(IndexName::from_str("pytorch").unwrap()),
879 url: IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(),
880 cache_control: Some(IndexCacheControl {
881 api: Some(HeaderValue::from_static("no-cache")),
882 files: Some(HeaderValue::from_static("max-age=3600")),
883 }),
884 explicit: false,
885 default: false,
886 origin: None,
887 format: IndexFormat::Simple,
888 publish_url: None,
889 authenticate: uv_auth::AuthPolicy::default(),
890 ignore_error_codes: None,
891 exclude_newer: None,
892 }];
893
894 let index_urls = IndexUrls::from_indexes(indexes.clone());
895 let index_locations = IndexLocations::new(indexes, Vec::new(), false);
896
897 let pytorch_url = IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap();
898
899 assert_eq!(
901 index_urls.simple_api_cache_control_for(&pytorch_url),
902 Some(HeaderValue::from_static("no-cache"))
903 );
904 assert_eq!(
905 index_urls.artifact_cache_control_for(&pytorch_url),
906 Some(HeaderValue::from_static("max-age=3600"))
907 );
908
909 assert_eq!(
911 index_locations.simple_api_cache_control_for(&pytorch_url),
912 Some(HeaderValue::from_static("no-cache"))
913 );
914 assert_eq!(
915 index_locations.artifact_cache_control_for(&pytorch_url),
916 Some(HeaderValue::from_static("max-age=3600"))
917 );
918 }
919
920 #[test]
921 fn test_nvidia_default_cache_control() {
922 let indexes = vec![Index {
924 name: Some(IndexName::from_str("nvidia").unwrap()),
925 url: IndexUrl::from_str("https://pypi.nvidia.com").unwrap(),
926 cache_control: None, explicit: false,
928 default: false,
929 origin: None,
930 format: IndexFormat::Simple,
931 publish_url: None,
932 authenticate: uv_auth::AuthPolicy::default(),
933 ignore_error_codes: None,
934 exclude_newer: None,
935 }];
936
937 let index_urls = IndexUrls::from_indexes(indexes.clone());
938 let index_locations = IndexLocations::new(indexes, Vec::new(), false);
939
940 let nvidia_url = IndexUrl::from_str("https://pypi.nvidia.com").unwrap();
941
942 assert_eq!(index_urls.simple_api_cache_control_for(&nvidia_url), None);
944 assert_eq!(
945 index_urls.artifact_cache_control_for(&nvidia_url),
946 Some(HeaderValue::from_static(
947 "max-age=365000000, immutable, public",
948 ))
949 );
950
951 assert_eq!(
953 index_locations.simple_api_cache_control_for(&nvidia_url),
954 None
955 );
956 assert_eq!(
957 index_locations.artifact_cache_control_for(&nvidia_url),
958 Some(HeaderValue::from_static(
959 "max-age=365000000, immutable, public",
960 ))
961 );
962 }
963}