Skip to main content

polars_io/cloud/
options.rs

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/// The type of the config keys must satisfy the following requirements:
63/// 1. must be easily collected into a HashMap, the type required by the object_crate API.
64/// 2. be Serializable, required when the serde-lazy feature is defined.
65/// 3. not actually use HashMap since that type is disallowed in Polars for performance reasons.
66///
67/// Currently this type is a vector of pairs config key - config value.
68#[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))]
102/// Options to connect to various cloud providers.
103pub 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    /// Note: In most cases you will want to access this via [`CloudOptions::initialized_credential_provider`]
113    /// rather than directly.
114    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/// Rate-limit config publicly exposed through storage_options.
212#[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)]
248/// Parse an untype configuration hashmap to a typed configuration for the given configuration key type.
249fn 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        // Silently ignores custom upstream storage_options
258        .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    /// URI with 'file:' scheme
271    File,
272    /// Google cloud platform
273    Gcp,
274    Http,
275    /// HuggingFace
276    Hf,
277    /// Externally registered scheme (e.g. hdfs:// as "hdfs")
278    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        // Disables the time limit for downloading the response body.
315        .with_timeout_disabled()
316        // Set the time limit for establishing the connection.
317        .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    /// Set the configuration for AWS connections. This is the preferred API from rust.
390    #[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    /// Build the [`object_store::ObjectStore`] implementation for AWS.
402    #[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                        // Set a default value if the endpoint is not aws.
494                        // See: #13042
495                        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            // AWS default checksum, which is also more efficient than SHA256.
558            builder.with_checksum_algorithm(object_store::aws::Checksum::CRC64NVME)
559        } else {
560            builder
561        };
562
563        // Insert HTTP middleware with rate-limiter.
564        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    /// Set the configuration for Azure connections. This is the preferred API from rust.
582    #[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    /// Build the [`object_store::ObjectStore`] implementation for Azure.
594    #[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        // The credential provider `self.credentials` is prioritized if it is set. We also need
609        // `from_env()` as it may source environment configured storage account name.
610        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        // Insert HTTP middleware with rate-limiter.
640        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    /// Set the configuration for GCP connections. This is the preferred API from rust.
657    #[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    /// Build the [`object_store::ObjectStore`] implementation for GCP.
669    #[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        // Insert HTTP middleware with rate-limiter.
710        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    /// Parse a configuration from a Hashmap. This is the interface from Python.
746    #[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    /// Python passes a credential provider builder that needs to be called to get the actual credential
871    /// provider.
872    #[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}