1#[cfg(feature = "aws")]
2use std::io::Read;
3#[cfg(feature = "aws")]
4use std::path::Path;
5use std::str::FromStr;
6#[cfg(any(feature = "aws", feature = "gcp", feature = "azure", feature = "http"))]
7use std::sync::Arc;
8use std::sync::LazyLock;
9
10#[cfg(any(feature = "aws", feature = "gcp", feature = "azure", feature = "http"))]
11use object_store::ClientOptions;
12#[cfg(feature = "aws")]
13use object_store::aws::AmazonS3Builder;
14#[cfg(feature = "aws")]
15pub use object_store::aws::AmazonS3ConfigKey;
16#[cfg(feature = "azure")]
17pub use object_store::azure::AzureConfigKey;
18#[cfg(feature = "azure")]
19use object_store::azure::MicrosoftAzureBuilder;
20#[cfg(feature = "gcp")]
21use object_store::gcp::GoogleCloudStorageBuilder;
22#[cfg(feature = "gcp")]
23pub use object_store::gcp::GoogleConfigKey;
24use polars_error::*;
25#[cfg(feature = "aws")]
26use polars_utils::cache::LruCache;
27use polars_utils::pl_path::{CloudScheme, PlRefPath};
28use polars_utils::total_ord::TotalOrdWrap;
29#[cfg(feature = "http")]
30use reqwest::header::HeaderMap;
31#[cfg(feature = "serde")]
32use serde::{Deserialize, Serialize};
33
34#[cfg(feature = "cloud")]
35use super::credential_provider::PlCredentialProvider;
36#[cfg(feature = "cloud")]
37use crate::cloud::ObjectStoreErrorContext;
38#[cfg(any(feature = "aws", feature = "gcp", feature = "azure", feature = "http"))]
39use crate::cloud::dns::{CachingResolver, DnsResolverConfig};
40#[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))]
41use crate::cloud::http_rate_limit::PacedHttpConnector;
42#[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))]
43use crate::cloud::http_rate_limit::RateLimiter;
44#[cfg(feature = "file_cache")]
45use crate::file_cache::get_env_file_cache_ttl;
46#[cfg(feature = "aws")]
47use crate::pl_async::with_concurrency_budget;
48
49#[cfg(feature = "aws")]
50fn to_io_err(err: reqwest::Error) -> PolarsError {
51 PolarsError::IO {
52 error: Arc::new(std::io::Error::other(err)),
53 msg: None,
54 }
55}
56
57#[cfg(feature = "aws")]
58static BUCKET_REGION: LazyLock<
59 std::sync::Mutex<LruCache<polars_utils::pl_str::PlSmallStr, polars_utils::pl_str::PlSmallStr>>,
60> = LazyLock::new(|| std::sync::Mutex::new(LruCache::with_capacity(32)));
61
62#[allow(dead_code)]
69type Configs<T> = Vec<(T, String)>;
70
71#[derive(Clone, Debug, PartialEq, Hash, Eq)]
72#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
73#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
74pub enum CloudConfig {
75 #[cfg(feature = "aws")]
76 Aws(
77 #[cfg_attr(feature = "dsl-schema", schemars(with = "Vec<(String, String)>"))]
78 Configs<AmazonS3ConfigKey>,
79 ),
80 #[cfg(feature = "azure")]
81 Azure(
82 #[cfg_attr(feature = "dsl-schema", schemars(with = "Vec<(String, String)>"))]
83 Configs<AzureConfigKey>,
84 ),
85 #[cfg(feature = "gcp")]
86 Gcp(
87 #[cfg_attr(feature = "dsl-schema", schemars(with = "Vec<(String, String)>"))]
88 Configs<GoogleConfigKey>,
89 ),
90 #[cfg(feature = "http")]
91 Http {
92 headers: Vec<(String, String)>,
93 },
94 Ext {
95 options: Vec<(String, String)>,
96 },
97}
98
99#[derive(Clone, Debug, PartialEq, Hash, Eq)]
100#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
101#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
102pub struct CloudOptions {
104 #[cfg(feature = "file_cache")]
105 pub file_cache_ttl: u64,
106 pub config: Option<CloudConfig>,
107 #[cfg_attr(feature = "serde", serde(default))]
108 pub retry_config: CloudRetryConfig,
109 #[cfg_attr(feature = "serde", serde(default))]
110 pub rate_limit_config: CloudRateLimitConfig,
111 #[cfg(feature = "cloud")]
112 pub(crate) credential_provider: Option<PlCredentialProvider>,
115}
116
117impl Default for CloudOptions {
118 fn default() -> Self {
119 Self::default_static_ref().clone()
120 }
121}
122
123impl CloudOptions {
124 pub fn default_static_ref() -> &'static Self {
125 static DEFAULT: LazyLock<CloudOptions> = LazyLock::new(|| CloudOptions {
126 #[cfg(feature = "file_cache")]
127 file_cache_ttl: get_env_file_cache_ttl(),
128 config: None,
129 retry_config: CloudRetryConfig::default(),
130 rate_limit_config: CloudRateLimitConfig::default(),
131 #[cfg(feature = "cloud")]
132 credential_provider: None,
133 });
134
135 &DEFAULT
136 }
137}
138
139#[derive(Clone, Copy, Default, Debug, PartialEq, Hash, Eq)]
140#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
141#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
142pub struct CloudRetryConfig {
143 pub max_retries: Option<usize>,
144 pub retry_timeout: Option<std::time::Duration>,
145 pub retry_init_backoff: Option<std::time::Duration>,
146 pub retry_max_backoff: Option<std::time::Duration>,
147 pub retry_base_multiplier: Option<TotalOrdWrap<f64>>,
148}
149
150#[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))]
151impl From<CloudRetryConfig> for object_store::RetryConfig {
152 fn from(value: CloudRetryConfig) -> Self {
153 use std::time::Duration;
154
155 use polars_core::config::verbose;
156
157 let out = object_store::RetryConfig {
158 backoff: object_store::BackoffConfig {
159 init_backoff: value
160 .retry_init_backoff
161 .unwrap_or_else(|| DEFAULTS.backoff.init_backoff),
162 max_backoff: value
163 .retry_max_backoff
164 .unwrap_or_else(|| DEFAULTS.backoff.max_backoff),
165 base: value
166 .retry_base_multiplier
167 .map_or_else(|| DEFAULTS.backoff.base, |x| x.0),
168 },
169 max_retries: value.max_retries.unwrap_or_else(|| DEFAULTS.max_retries),
170 retry_timeout: value
171 .retry_timeout
172 .unwrap_or_else(|| DEFAULTS.retry_timeout),
173 };
174
175 if verbose() {
176 eprintln!("object-store retry config: {:?}", out)
177 }
178
179 return out;
180
181 static DEFAULTS: LazyLock<object_store::RetryConfig> =
182 LazyLock::new(|| object_store::RetryConfig {
183 backoff: object_store::BackoffConfig {
184 init_backoff: Duration::from_millis(parse_env_var(
185 100,
186 "POLARS_CLOUD_RETRY_INIT_BACKOFF_MS",
187 )),
188 max_backoff: Duration::from_millis(parse_env_var(
189 15 * 1000,
190 "POLARS_CLOUD_RETRY_MAX_BACKOFF_MS",
191 )),
192 base: parse_env_var(2., "POLARS_CLOUD_RETRY_BASE_MULTIPLIER"),
193 },
194 max_retries: parse_env_var(2, "POLARS_CLOUD_MAX_RETRIES"),
195 retry_timeout: Duration::from_millis(parse_env_var(
196 10 * 1000,
197 "POLARS_CLOUD_RETRY_TIMEOUT_MS",
198 )),
199 });
200
201 fn parse_env_var<T: FromStr>(default: T, name: &'static str) -> T {
202 std::env::var(name).map_or(default, |x| {
203 x.parse::<T>()
204 .ok()
205 .unwrap_or_else(|| panic!("invalid value for {name}: {x}"))
206 })
207 }
208 }
209}
210
211#[derive(Clone, Copy, Default, Debug, PartialEq, Hash, Eq)]
213#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
214#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
215pub struct CloudDirectionalRateLimitConfig {
216 pub init_rate: Option<u64>,
217 pub floor_rate: Option<u64>,
218 pub ceiling_rate: Option<u64>,
219}
220
221#[derive(Clone, Copy, Default, Debug, PartialEq, Hash, Eq)]
222#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
223#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
224pub struct CloudRateLimitConfig {
225 pub read: CloudDirectionalRateLimitConfig,
226 pub write: CloudDirectionalRateLimitConfig,
227}
228
229#[cfg(feature = "http")]
230pub(crate) fn try_build_http_header_map_from_items_slice<S: AsRef<str>>(
231 headers: &[(S, S)],
232) -> PolarsResult<HeaderMap> {
233 use reqwest::header::{HeaderName, HeaderValue};
234
235 let mut map = HeaderMap::with_capacity(headers.len());
236 for (k, v) in headers {
237 let (k, v) = (k.as_ref(), v.as_ref());
238 map.insert(
239 HeaderName::from_str(k).map_err(to_compute_err)?,
240 HeaderValue::from_str(v).map_err(to_compute_err)?,
241 );
242 }
243
244 Ok(map)
245}
246
247#[allow(dead_code)]
248fn parse_untyped_config<T, I: IntoIterator<Item = (impl AsRef<str>, impl Into<String>)>>(
250 config: I,
251) -> PolarsResult<Configs<T>>
252where
253 T: FromStr + Eq + std::hash::Hash,
254{
255 Ok(config
256 .into_iter()
257 .filter_map(|(key, val)| {
259 T::from_str(key.as_ref().to_ascii_lowercase().as_str())
260 .ok()
261 .map(|typed_key| (typed_key, val.into()))
262 })
263 .collect::<Configs<T>>())
264}
265
266#[derive(Debug, Copy, Clone, PartialEq)]
267pub enum CloudType {
268 Aws,
269 Azure,
270 File,
272 Gcp,
274 Http,
275 Hf,
277 Ext(&'static str),
279}
280
281impl CloudType {
282 pub fn from_cloud_scheme(scheme: CloudScheme) -> Self {
283 match scheme {
284 CloudScheme::Abfs
285 | CloudScheme::Abfss
286 | CloudScheme::Adl
287 | CloudScheme::Az
288 | CloudScheme::Azure => Self::Azure,
289
290 CloudScheme::File | CloudScheme::FileNoHostname => Self::File,
291
292 CloudScheme::Gcs | CloudScheme::Gs => Self::Gcp,
293
294 CloudScheme::Hf => Self::Hf,
295
296 CloudScheme::Http | CloudScheme::Https => Self::Http,
297
298 CloudScheme::S3 | CloudScheme::S3a => Self::Aws,
299
300 CloudScheme::Ext(scheme) => Self::Ext(scheme),
301 }
302 }
303}
304
305pub static USER_AGENT: &str = concat!("polars", "/", env!("CARGO_PKG_VERSION"),);
306
307#[cfg(any(feature = "aws", feature = "gcp", feature = "azure", feature = "http"))]
308pub(super) fn get_client_options() -> ClientOptions {
309 use std::num::NonZeroU64;
310
311 use reqwest::header::HeaderValue;
312
313 ClientOptions::new()
314 .with_timeout_disabled()
316 .with_connect_timeout(std::time::Duration::from_secs(
318 std::env::var("POLARS_HTTP_CONNECT_TIMEOUT_SECONDS")
319 .map(|x| {
320 x.parse::<NonZeroU64>()
321 .ok()
322 .unwrap_or_else(|| {
323 panic!("invalid value for POLARS_HTTP_CONNECT_TIMEOUT_SECONDS: {x}")
324 })
325 .get()
326 })
327 .unwrap_or(5 * 60),
328 ))
329 .with_user_agent(HeaderValue::from_static(USER_AGENT))
330 .with_allow_http(true)
331 .with_dns_resolver(Arc::new(
332 CachingResolver::new(DnsResolverConfig::from_env()),
333 ))
334}
335
336#[cfg(feature = "aws")]
337fn read_config(
338 builder: &mut AmazonS3Builder,
339 items: &[(&Path, &[(&str, AmazonS3ConfigKey)])],
340) -> Option<()> {
341 use crate::path_utils::resolve_homedir;
342
343 for (path, keys) in items {
344 if keys
345 .iter()
346 .all(|(_, key)| builder.get_config_value(key).is_some())
347 {
348 continue;
349 }
350
351 let mut config = std::fs::File::open(resolve_homedir(path)).ok()?;
352 let mut buf = vec![];
353 config.read_to_end(&mut buf).ok()?;
354 let content = std::str::from_utf8(buf.as_ref()).ok()?;
355
356 for (pattern, key) in keys.iter() {
357 if builder.get_config_value(key).is_none() {
358 let reg = polars_utils::regex_cache::compile_regex(pattern).unwrap();
359 let cap = reg.captures(content)?;
360 let m = cap.get(1)?;
361 let parsed = m.as_str();
362 *builder = std::mem::take(builder).with_config(*key, parsed);
363 }
364 }
365 }
366 Some(())
367}
368
369impl CloudOptions {
370 pub fn with_retry_config(mut self, retry_config: CloudRetryConfig) -> Self {
371 self.retry_config = retry_config;
372 self
373 }
374
375 pub fn with_rate_limit_config(mut self, rate_limit_config: CloudRateLimitConfig) -> Self {
376 self.rate_limit_config = rate_limit_config;
377 self
378 }
379
380 #[cfg(feature = "cloud")]
381 pub fn with_credential_provider(
382 mut self,
383 credential_provider: Option<PlCredentialProvider>,
384 ) -> Self {
385 self.credential_provider = credential_provider;
386 self
387 }
388
389 #[cfg(feature = "aws")]
391 pub fn with_aws<I: IntoIterator<Item = (AmazonS3ConfigKey, impl Into<String>)>>(
392 mut self,
393 configs: I,
394 ) -> Self {
395 self.config = Some(CloudConfig::Aws(
396 configs.into_iter().map(|(k, v)| (k, v.into())).collect(),
397 ));
398 self
399 }
400
401 #[cfg(feature = "aws")]
403 pub(crate) async fn build_aws(
404 &self,
405 url: PlRefPath,
406 clear_cached_credentials: bool,
407 rate_limiter: Option<&RateLimiter>,
408 ) -> PolarsResult<impl object_store::ObjectStore> {
409 use object_store::client::ReqwestConnector;
410
411 use super::credential_provider::IntoCredentialProvider;
412
413 let opt_credential_provider =
414 self.initialized_credential_provider(clear_cached_credentials)?;
415
416 let mut builder = AmazonS3Builder::from_env()
417 .with_client_options(get_client_options())
418 .with_url(url.clone().to_string());
419
420 if let Some(credential_provider) = &opt_credential_provider {
421 let storage_update_options = parse_untyped_config::<AmazonS3ConfigKey, _>(
422 credential_provider
423 .storage_update_options()?
424 .into_iter()
425 .map(|(k, v)| (k, v.to_string())),
426 )?;
427
428 for (key, value) in storage_update_options {
429 builder = builder.with_config(key, value);
430 }
431 }
432
433 read_config(
434 &mut builder,
435 &[(
436 Path::new("~/.aws/config"),
437 &[("region\\s*=\\s*([^\r\n]*)", AmazonS3ConfigKey::Region)],
438 )],
439 );
440
441 read_config(
442 &mut builder,
443 &[(
444 Path::new("~/.aws/credentials"),
445 &[
446 (
447 "aws_access_key_id\\s*=\\s*([^\\r\\n]*)",
448 AmazonS3ConfigKey::AccessKeyId,
449 ),
450 (
451 "aws_secret_access_key\\s*=\\s*([^\\r\\n]*)",
452 AmazonS3ConfigKey::SecretAccessKey,
453 ),
454 (
455 "aws_session_token\\s*=\\s*([^\\r\\n]*)",
456 AmazonS3ConfigKey::Token,
457 ),
458 ],
459 )],
460 );
461
462 if let Some(options) = &self.config {
463 let CloudConfig::Aws(options) = options else {
464 panic!("impl error: cloud type mismatch")
465 };
466 for (key, value) in options {
467 builder = builder.with_config(*key, value);
468 }
469 }
470
471 if builder
472 .get_config_value(&AmazonS3ConfigKey::DefaultRegion)
473 .is_none()
474 && builder
475 .get_config_value(&AmazonS3ConfigKey::Region)
476 .is_none()
477 {
478 let bucket = crate::cloud::CloudLocation::new(url.clone(), false)?.bucket;
479 let region = {
480 let mut bucket_region = BUCKET_REGION.lock().unwrap();
481 bucket_region.get(bucket.as_str()).cloned()
482 };
483
484 match region {
485 Some(region) => {
486 builder = builder.with_config(AmazonS3ConfigKey::Region, region.as_str())
487 },
488 None => {
489 if builder
490 .get_config_value(&AmazonS3ConfigKey::Endpoint)
491 .is_some()
492 {
493 builder = builder.with_config(AmazonS3ConfigKey::Region, "us-east-1");
496 } else {
497 polars_warn!(
498 "'(default_)region' not set; polars will try to get it from bucket\n\nSet the region manually to silence this warning."
499 );
500 let result = with_concurrency_budget(1, || async {
501 reqwest::Client::builder()
502 .user_agent(USER_AGENT)
503 .build()
504 .unwrap()
505 .head(format!("https://{bucket}.s3.amazonaws.com"))
506 .send()
507 .await
508 .map_err(to_io_err)
509 })
510 .await?;
511 if let Some(region) = result.headers().get("x-amz-bucket-region") {
512 let region =
513 std::str::from_utf8(region.as_bytes()).map_err(to_compute_err)?;
514 let mut bucket_region = BUCKET_REGION.lock().unwrap();
515 bucket_region.insert(bucket, region.into());
516 builder = builder.with_config(AmazonS3ConfigKey::Region, region)
517 }
518 }
519 },
520 };
521 };
522
523 let builder = builder.with_retry(self.retry_config.into());
524
525 let opt_credential_provider = match opt_credential_provider {
526 #[cfg(feature = "python")]
527 Some(PlCredentialProvider::Python(object)) => {
528 if pyo3::Python::attach(|py| {
529 let Ok(func_object) = object
530 .unwrap_as_provider_ref()
531 .getattr(py, "_can_use_as_provider")
532 else {
533 return PolarsResult::Ok(true);
534 };
535
536 Ok(func_object.call0(py)?.extract::<bool>(py).unwrap())
537 })? {
538 Some(PlCredentialProvider::Python(object))
539 } else {
540 None
541 }
542 },
543
544 v => v,
545 };
546
547 let builder = if let Some(credential_provider) = opt_credential_provider {
548 builder.with_credentials(credential_provider.into_aws_provider())
549 } else {
550 builder
551 };
552
553 let builder = if builder
554 .get_config_value(&AmazonS3ConfigKey::Checksum)
555 .is_none()
556 {
557 builder.with_checksum_algorithm(object_store::aws::Checksum::CRC64NVME)
559 } else {
560 builder
561 };
562
563 let builder = match &rate_limiter {
565 Some(rate_limiter) => {
566 let paced_http_connector =
567 PacedHttpConnector::new(Box::new(ReqwestConnector::default()), rate_limiter);
568 builder.with_http_connector(paced_http_connector)
569 },
570 None => builder,
571 };
572
573 let out = builder
574 .with_unsigned_payload(true)
575 .build()
576 .map_err(|e| ObjectStoreErrorContext::new(url).attach_err_info(e))?;
577
578 Ok(out)
579 }
580
581 #[cfg(feature = "azure")]
583 pub fn with_azure<I: IntoIterator<Item = (AzureConfigKey, impl Into<String>)>>(
584 mut self,
585 configs: I,
586 ) -> Self {
587 self.config = Some(CloudConfig::Azure(
588 configs.into_iter().map(|(k, v)| (k, v.into())).collect(),
589 ));
590 self
591 }
592
593 #[cfg(feature = "azure")]
595 pub(crate) fn build_azure(
596 &self,
597 url: PlRefPath,
598 clear_cached_credentials: bool,
599 rate_limiter: Option<&RateLimiter>,
600 ) -> PolarsResult<impl object_store::ObjectStore> {
601 use object_store::client::ReqwestConnector;
602
603 use super::credential_provider::IntoCredentialProvider;
604 use crate::cloud::ObjectStoreErrorContext;
605
606 let verbose = polars_core::config::verbose();
607
608 let mut builder =
611 MicrosoftAzureBuilder::from_env().with_client_options(get_client_options());
612
613 if let Some(options) = &self.config {
614 let CloudConfig::Azure(options) = options else {
615 panic!("impl error: cloud type mismatch")
616 };
617 for (key, value) in options.iter() {
618 builder = builder.with_config(*key, value);
619 }
620 }
621
622 let builder = builder
623 .with_url(url.to_string())
624 .with_retry(self.retry_config.into());
625
626 let builder =
627 if let Some(v) = self.initialized_credential_provider(clear_cached_credentials)? {
628 if verbose {
629 eprintln!(
630 "[CloudOptions::build_azure]: Using credential provider {:?}",
631 v
632 );
633 }
634 builder.with_credentials(v.into_azure_provider())
635 } else {
636 builder
637 };
638
639 let builder = match &rate_limiter {
641 Some(rate_limiter) => {
642 let paced_http_connector =
643 PacedHttpConnector::new(Box::new(ReqwestConnector::default()), rate_limiter);
644 builder.with_http_connector(paced_http_connector)
645 },
646 None => builder,
647 };
648
649 let out = builder
650 .build()
651 .map_err(|e| ObjectStoreErrorContext::new(url).attach_err_info(e))?;
652
653 Ok(out)
654 }
655
656 #[cfg(feature = "gcp")]
658 pub fn with_gcp<I: IntoIterator<Item = (GoogleConfigKey, impl Into<String>)>>(
659 mut self,
660 configs: I,
661 ) -> Self {
662 self.config = Some(CloudConfig::Gcp(
663 configs.into_iter().map(|(k, v)| (k, v.into())).collect(),
664 ));
665 self
666 }
667
668 #[cfg(feature = "gcp")]
670 pub(crate) fn build_gcp(
671 &self,
672 url: PlRefPath,
673 clear_cached_credentials: bool,
674 rate_limiter: Option<&RateLimiter>,
675 ) -> PolarsResult<impl object_store::ObjectStore> {
676 use object_store::client::ReqwestConnector;
677
678 use super::credential_provider::IntoCredentialProvider;
679
680 let credential_provider = self.initialized_credential_provider(clear_cached_credentials)?;
681
682 let builder = if credential_provider.is_none() {
683 GoogleCloudStorageBuilder::from_env()
684 } else {
685 GoogleCloudStorageBuilder::new()
686 };
687
688 let mut builder = builder.with_client_options(get_client_options());
689
690 if let Some(options) = &self.config {
691 let CloudConfig::Gcp(options) = options else {
692 panic!("impl error: cloud type mismatch")
693 };
694 for (key, value) in options.iter() {
695 builder = builder.with_config(*key, value);
696 }
697 }
698
699 let builder = builder
700 .with_url(url.to_string())
701 .with_retry(self.retry_config.into());
702
703 let builder = if let Some(v) = credential_provider {
704 builder.with_credentials(v.into_gcp_provider())
705 } else {
706 builder
707 };
708
709 let builder = match &rate_limiter {
711 Some(rate_limiter) => {
712 let paced_http_connector =
713 PacedHttpConnector::new(Box::new(ReqwestConnector::default()), rate_limiter);
714 builder.with_http_connector(paced_http_connector)
715 },
716 None => builder,
717 };
718
719 let out = builder
720 .build()
721 .map_err(|e| ObjectStoreErrorContext::new(url).attach_err_info(e))?;
722
723 Ok(out)
724 }
725
726 #[cfg(feature = "http")]
727 pub fn build_http(&self, url: PlRefPath) -> PolarsResult<impl object_store::ObjectStore> {
728 let out = object_store::http::HttpBuilder::new()
729 .with_url(url.to_string())
730 .with_client_options({
731 let mut opts = super::get_client_options();
732 if let Some(CloudConfig::Http { headers }) = &self.config {
733 opts = opts.with_default_headers(try_build_http_header_map_from_items_slice(
734 headers.as_slice(),
735 )?);
736 }
737 opts
738 })
739 .build()
740 .map_err(|e| ObjectStoreErrorContext::new(url).attach_err_info(e))?;
741
742 Ok(out)
743 }
744
745 #[allow(unused_variables)]
747 pub fn from_untyped_config<I: IntoIterator<Item = (impl AsRef<str>, impl Into<String>)>>(
748 scheme: Option<CloudScheme>,
749 config: I,
750 ) -> PolarsResult<Self> {
751 match scheme.map_or(CloudType::File, CloudType::from_cloud_scheme) {
752 CloudType::Aws => {
753 #[cfg(feature = "aws")]
754 {
755 parse_untyped_config::<AmazonS3ConfigKey, _>(config)
756 .map(|aws| Self::default().with_aws(aws))
757 }
758 #[cfg(not(feature = "aws"))]
759 {
760 polars_bail!(ComputeError: "'aws' feature is not enabled");
761 }
762 },
763 CloudType::Azure => {
764 #[cfg(feature = "azure")]
765 {
766 parse_untyped_config::<AzureConfigKey, _>(config)
767 .map(|azure| Self::default().with_azure(azure))
768 }
769 #[cfg(not(feature = "azure"))]
770 {
771 polars_bail!(ComputeError: "'azure' feature is not enabled");
772 }
773 },
774 CloudType::File => Ok(Self::default()),
775 CloudType::Http => Ok(Self::default()),
776 CloudType::Gcp => {
777 #[cfg(feature = "gcp")]
778 {
779 parse_untyped_config::<GoogleConfigKey, _>(config)
780 .map(|gcp| Self::default().with_gcp(gcp))
781 }
782 #[cfg(not(feature = "gcp"))]
783 {
784 polars_bail!(ComputeError: "'gcp' feature is not enabled");
785 }
786 },
787 CloudType::Hf => {
788 #[cfg(feature = "http")]
789 {
790 use polars_core::config;
791
792 use crate::path_utils::resolve_homedir;
793
794 let mut this = Self::default();
795 let mut token = None;
796 let verbose = config::verbose();
797
798 for (i, (k, v)) in config.into_iter().enumerate() {
799 let (k, v) = (k.as_ref(), v.into());
800
801 if i == 0 && k == "token" {
802 if verbose {
803 eprintln!("HF token sourced from storage_options");
804 }
805 token = Some(v);
806 } else {
807 polars_bail!(ComputeError: "unknown configuration key for HF: {}", k)
808 }
809 }
810
811 token = token
812 .or_else(|| {
813 let v = std::env::var("HF_TOKEN").ok();
814 if v.is_some() && verbose {
815 eprintln!("HF token sourced from HF_TOKEN env var");
816 }
817 v
818 })
819 .or_else(|| {
820 let hf_home = std::env::var("HF_HOME");
821 let hf_home = hf_home.as_deref();
822 let hf_home = hf_home.unwrap_or("~/.cache/huggingface");
823 let hf_home = resolve_homedir(hf_home);
824 let cached_token_path = hf_home.join("token");
825
826 let v = std::string::String::from_utf8(
827 std::fs::read(&cached_token_path).ok()?,
828 )
829 .ok()
830 .filter(|x| !x.is_empty());
831
832 if v.is_some() && verbose {
833 eprintln!("HF token sourced from {:?}", cached_token_path);
834 }
835
836 v
837 });
838
839 if let Some(v) = token {
840 this.config = Some(CloudConfig::Http {
841 headers: vec![("Authorization".into(), format!("Bearer {v}"))],
842 })
843 }
844
845 Ok(this)
846 }
847 #[cfg(not(feature = "http"))]
848 {
849 polars_bail!(ComputeError: "'http' feature is not enabled");
850 }
851 },
852 CloudType::Ext(_) => {
853 let pairs: Vec<(String, String)> = config
854 .into_iter()
855 .map(|(k, v)| (k.as_ref().to_string(), v.into()))
856 .collect();
857
858 Ok(Self {
859 config: if pairs.is_empty() {
860 None
861 } else {
862 Some(CloudConfig::Ext { options: pairs })
863 },
864 ..Self::default()
865 })
866 },
867 }
868 }
869
870 #[cfg(feature = "cloud")]
873 fn initialized_credential_provider(
874 &self,
875 clear_cached_credentials: bool,
876 ) -> PolarsResult<Option<PlCredentialProvider>> {
877 if let Some(v) = self.credential_provider.clone() {
878 v.try_into_initialized(clear_cached_credentials)
879 } else {
880 Ok(None)
881 }
882 }
883}
884
885#[cfg(feature = "cloud")]
886#[cfg(test)]
887mod tests {
888 use hashbrown::HashMap;
889
890 use super::parse_untyped_config;
891
892 #[cfg(feature = "aws")]
893 #[test]
894 fn test_parse_untyped_config() {
895 use object_store::aws::AmazonS3ConfigKey;
896
897 let aws_config = [
898 ("aws_secret_access_key", "a_key"),
899 ("aws_s3_allow_unsafe_rename", "true"),
900 ]
901 .into_iter()
902 .collect::<HashMap<_, _>>();
903 let aws_keys = parse_untyped_config::<AmazonS3ConfigKey, _>(aws_config)
904 .expect("Parsing keys shouldn't have thrown an error");
905
906 assert_eq!(
907 aws_keys.first().unwrap().0,
908 AmazonS3ConfigKey::SecretAccessKey
909 );
910 assert_eq!(aws_keys.len(), 1);
911
912 let aws_config = [
913 ("AWS_SECRET_ACCESS_KEY", "a_key"),
914 ("aws_s3_allow_unsafe_rename", "true"),
915 ]
916 .into_iter()
917 .collect::<HashMap<_, _>>();
918 let aws_keys = parse_untyped_config::<AmazonS3ConfigKey, _>(aws_config)
919 .expect("Parsing keys shouldn't have thrown an error");
920
921 assert_eq!(
922 aws_keys.first().unwrap().0,
923 AmazonS3ConfigKey::SecretAccessKey
924 );
925 assert_eq!(aws_keys.len(), 1);
926 }
927}