1use std::collections::BTreeMap;
2use std::fmt::Debug;
3use std::future::Future;
4use std::hash::Hash;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use async_trait::async_trait;
10#[cfg(feature = "aws")]
11pub use object_store::aws::AwsCredential;
12#[cfg(feature = "azure")]
13pub use object_store::azure::AzureCredential;
14#[cfg(feature = "gcp")]
15pub use object_store::gcp::GcpCredential;
16use polars_core::config;
17use polars_error::{PolarsResult, polars_bail, to_compute_err};
18use polars_utils::pl_str::PlSmallStr;
19#[cfg(feature = "python")]
20use polars_utils::python_function::PythonObject;
21#[cfg(feature = "python")]
22use python_impl::PythonCredentialProvider;
23
24#[derive(Clone, Debug, PartialEq, Hash, Eq)]
25pub enum PlCredentialProvider {
26 Function(CredentialProviderFunction),
28 #[cfg(feature = "python")]
29 Python(PythonCredentialProvider),
30 AutoInit(AutoInitConfig),
33}
34
35#[derive(Clone, Debug, PartialEq, Hash, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
43#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
44pub enum AutoInitConfig {
45 Aws {
46 profile_name: Option<PlSmallStr>,
47 region_name: Option<PlSmallStr>,
48 auto_init_unhandled_key: Option<PlSmallStr>,
49 storage_options: BTreeMap<PlSmallStr, Option<PlSmallStr>>,
50 },
51 Azure {
52 tenant_id: Option<PlSmallStr>,
53 storage_account: Option<PlSmallStr>,
54 },
55 Gcp {},
56 GcpToken {
57 token: PlSmallStr,
58 },
59}
60
61impl PlCredentialProvider {
62 pub fn from_func(
66 func: impl Fn() -> Pin<
69 Box<dyn Future<Output = PolarsResult<(ObjectStoreCredential, u64)>> + Send + Sync>,
70 > + Send
71 + Sync
72 + 'static,
73 ) -> Self {
74 Self::Function(CredentialProviderFunction(Arc::new(func)))
75 }
76
77 #[cfg(feature = "python")]
80 pub fn from_python_builder(func: pyo3::Py<pyo3::PyAny>) -> Self {
81 Self::Python(python_impl::PythonCredentialProvider::Builder(Arc::new(
82 PythonObject(func),
83 )))
84 }
85
86 #[cfg(feature = "python")]
87 pub fn from_auto_init_json(json: &str) -> PolarsResult<Self> {
88 serde_json::from_str(json).map(Self::AutoInit).map_err(
89 |e| polars_error::polars_err!(ComputeError: "invalid auto-init credential config: {e}"),
90 )
91 }
92
93 pub(crate) fn try_into_initialized(
98 self,
99 clear_cached_credentials: bool,
100 ) -> PolarsResult<Option<Self>> {
101 match self {
102 Self::Function(_) => Ok(Some(self)),
103 #[cfg(feature = "python")]
104 Self::Python(v) => Ok(v
105 .try_into_initialized(clear_cached_credentials)?
106 .map(Self::Python)),
107 Self::AutoInit(config) => config.try_into_initialized(clear_cached_credentials),
108 }
109 }
110
111 pub fn stable_cache_key(&self) -> PolarsResult<Vec<u8>> {
112 match self {
113 Self::Function(CredentialProviderFunction(v)) => Ok((Arc::as_ptr(v) as *const ()
114 as usize)
115 .to_ne_bytes()
116 .to_vec()),
117 #[cfg(feature = "python")]
118 Self::Python(v) => v.stable_cache_key(),
119 Self::AutoInit(config) => {
120 let json = serde_json::to_vec(config).map_err(to_compute_err)?;
122 let mut out = b"auto_init:".to_vec();
123 out.extend_from_slice(blake3::hash(&json).as_bytes());
124 Ok(out)
125 },
126 }
127 }
128}
129
130impl AutoInitConfig {
131 fn try_into_initialized(
132 self,
133 clear_cached_credentials: bool,
134 ) -> PolarsResult<Option<PlCredentialProvider>> {
135 #[cfg(feature = "gcp")]
137 if let Self::GcpToken { token } = &self {
138 let credential = Arc::new(object_store::gcp::GcpCredential {
139 bearer: token.to_string(),
140 });
141
142 return Ok(Some(PlCredentialProvider::from_func(move || {
143 let credential = credential.clone();
144 Box::pin(async move { Ok((ObjectStoreCredential::Gcp(credential), u64::MAX)) })
145 })));
146 }
147
148 #[cfg(feature = "python")]
149 {
150 Ok(
151 PythonCredentialProvider::build_auto_init(&self, clear_cached_credentials)?
152 .map(PlCredentialProvider::Python),
153 )
154 }
155
156 #[cfg(not(feature = "python"))]
160 {
161 let _ = clear_cached_credentials;
162
163 if let Self::Aws {
164 profile_name: Some(profile_name),
165 ..
166 } = &self
167 {
168 polars_bail!(
169 ComputeError:
170 "cannot load requested aws_profile '{profile_name}': requires Python"
171 )
172 }
173
174 Ok(None)
175 }
176 }
177}
178
179pub enum ObjectStoreCredential {
180 #[cfg(feature = "aws")]
181 Aws(Arc<object_store::aws::AwsCredential>),
182 #[cfg(feature = "azure")]
183 Azure(Arc<object_store::azure::AzureCredential>),
184 #[cfg(feature = "gcp")]
185 Gcp(Arc<object_store::gcp::GcpCredential>),
186 None,
188}
189
190impl ObjectStoreCredential {
191 fn variant_name(&self) -> &'static str {
192 match self {
193 #[cfg(feature = "aws")]
194 Self::Aws(_) => "Aws",
195 #[cfg(feature = "azure")]
196 Self::Azure(_) => "Azure",
197 #[cfg(feature = "gcp")]
198 Self::Gcp(_) => "Gcp",
199 Self::None => "None",
200 }
201 }
202
203 fn panic_type_mismatch(&self, expected: &str) {
204 panic!(
205 "impl error: credential type mismatch: expected {}, got {} instead",
206 expected,
207 self.variant_name()
208 )
209 }
210
211 #[cfg(feature = "aws")]
212 fn unwrap_aws(self) -> Arc<object_store::aws::AwsCredential> {
213 let Self::Aws(v) = self else {
214 self.panic_type_mismatch("aws");
215 unreachable!()
216 };
217 v
218 }
219
220 #[cfg(feature = "azure")]
221 fn unwrap_azure(self) -> Arc<object_store::azure::AzureCredential> {
222 let Self::Azure(v) = self else {
223 self.panic_type_mismatch("azure");
224 unreachable!()
225 };
226 v
227 }
228
229 #[cfg(feature = "gcp")]
230 fn unwrap_gcp(self) -> Arc<object_store::gcp::GcpCredential> {
231 let Self::Gcp(v) = self else {
232 self.panic_type_mismatch("gcp");
233 unreachable!()
234 };
235 v
236 }
237}
238
239pub trait IntoCredentialProvider: Sized {
240 #[cfg(feature = "aws")]
241 fn into_aws_provider(self) -> object_store::aws::AwsCredentialProvider {
242 unimplemented!()
243 }
244
245 #[cfg(feature = "azure")]
246 fn into_azure_provider(self) -> object_store::azure::AzureCredentialProvider {
247 unimplemented!()
248 }
249
250 #[cfg(feature = "gcp")]
251 fn into_gcp_provider(self) -> object_store::gcp::GcpCredentialProvider {
252 unimplemented!()
253 }
254
255 fn storage_update_options(&self) -> PolarsResult<Vec<(PlSmallStr, PlSmallStr)>>;
258}
259
260impl IntoCredentialProvider for PlCredentialProvider {
261 #[cfg(feature = "aws")]
262 fn into_aws_provider(self) -> object_store::aws::AwsCredentialProvider {
263 match self {
264 Self::Function(v) => v.into_aws_provider(),
265 #[cfg(feature = "python")]
266 Self::Python(v) => v.into_aws_provider(),
267 Self::AutoInit(_) => unreachable!("auto-init credential provider was not initialized"),
268 }
269 }
270
271 #[cfg(feature = "azure")]
272 fn into_azure_provider(self) -> object_store::azure::AzureCredentialProvider {
273 match self {
274 Self::Function(v) => v.into_azure_provider(),
275 #[cfg(feature = "python")]
276 Self::Python(v) => v.into_azure_provider(),
277 Self::AutoInit(_) => unreachable!("auto-init credential provider was not initialized"),
278 }
279 }
280
281 #[cfg(feature = "gcp")]
282 fn into_gcp_provider(self) -> object_store::gcp::GcpCredentialProvider {
283 match self {
284 Self::Function(v) => v.into_gcp_provider(),
285 #[cfg(feature = "python")]
286 Self::Python(v) => v.into_gcp_provider(),
287 Self::AutoInit(_) => unreachable!("auto-init credential provider was not initialized"),
288 }
289 }
290
291 fn storage_update_options(&self) -> PolarsResult<Vec<(PlSmallStr, PlSmallStr)>> {
292 match self {
293 Self::Function(v) => v.storage_update_options(),
294 #[cfg(feature = "python")]
295 Self::Python(v) => v.storage_update_options(),
296 Self::AutoInit(_) => unreachable!("auto-init credential provider was not initialized"),
297 }
298 }
299}
300
301type CredentialProviderFunctionImpl = Arc<
302 dyn Fn() -> Pin<
303 Box<dyn Future<Output = PolarsResult<(ObjectStoreCredential, u64)>> + Send + Sync>,
304 > + Send
305 + Sync,
306>;
307
308#[derive(Clone)]
310pub struct CredentialProviderFunction(CredentialProviderFunctionImpl);
311
312macro_rules! build_to_object_store_err {
313 ($s:expr) => {{
314 fn to_object_store_err(
315 e: impl std::error::Error + Send + Sync + 'static,
316 ) -> object_store::Error {
317 object_store::Error::Generic {
318 store: $s,
319 source: Box::new(e),
320 }
321 }
322
323 to_object_store_err
324 }};
325}
326
327impl IntoCredentialProvider for CredentialProviderFunction {
328 #[cfg(feature = "aws")]
329 fn into_aws_provider(self) -> object_store::aws::AwsCredentialProvider {
330 #[derive(Debug)]
331 struct S(
332 CredentialProviderFunction,
333 FetchedCredentialsCache<Arc<object_store::aws::AwsCredential>>,
334 );
335
336 #[async_trait]
337 impl object_store::CredentialProvider for S {
338 type Credential = object_store::aws::AwsCredential;
339
340 async fn get_credential(&self) -> object_store::Result<Arc<Self::Credential>> {
341 self.1
342 .get_maybe_update(async {
343 let (creds, expiry) = self.0.0().await?;
344 PolarsResult::Ok((creds.unwrap_aws(), expiry))
345 })
346 .await
347 .map_err(build_to_object_store_err!("credential-provider-aws"))
348 }
349 }
350
351 Arc::new(S(
352 self,
353 FetchedCredentialsCache::new(Arc::new(AwsCredential {
354 key_id: String::new(),
355 secret_key: String::new(),
356 token: None,
357 })),
358 ))
359 }
360
361 #[cfg(feature = "azure")]
362 fn into_azure_provider(self) -> object_store::azure::AzureCredentialProvider {
363 #[derive(Debug)]
364 struct S(
365 CredentialProviderFunction,
366 FetchedCredentialsCache<Arc<object_store::azure::AzureCredential>>,
367 );
368
369 #[async_trait]
370 impl object_store::CredentialProvider for S {
371 type Credential = object_store::azure::AzureCredential;
372
373 async fn get_credential(&self) -> object_store::Result<Arc<Self::Credential>> {
374 self.1
375 .get_maybe_update(async {
376 let (creds, expiry) = self.0.0().await?;
377 PolarsResult::Ok((creds.unwrap_azure(), expiry))
378 })
379 .await
380 .map_err(build_to_object_store_err!("credential-provider-azure"))
381 }
382 }
383
384 Arc::new(S(
385 self,
386 FetchedCredentialsCache::new(Arc::new(AzureCredential::BearerToken(String::new()))),
387 ))
388 }
389
390 #[cfg(feature = "gcp")]
391 fn into_gcp_provider(self) -> object_store::gcp::GcpCredentialProvider {
392 #[derive(Debug)]
393 struct S(
394 CredentialProviderFunction,
395 FetchedCredentialsCache<Arc<object_store::gcp::GcpCredential>>,
396 );
397
398 #[async_trait]
399 impl object_store::CredentialProvider for S {
400 type Credential = object_store::gcp::GcpCredential;
401
402 async fn get_credential(&self) -> object_store::Result<Arc<Self::Credential>> {
403 self.1
404 .get_maybe_update(async {
405 let (creds, expiry) = self.0.0().await?;
406 PolarsResult::Ok((creds.unwrap_gcp(), expiry))
407 })
408 .await
409 .map_err(build_to_object_store_err!("credential-provider-gcp"))
410 }
411 }
412
413 Arc::new(S(
414 self,
415 FetchedCredentialsCache::new(Arc::new(GcpCredential {
416 bearer: String::new(),
417 })),
418 ))
419 }
420
421 fn storage_update_options(&self) -> PolarsResult<Vec<(PlSmallStr, PlSmallStr)>> {
422 Ok(vec![])
423 }
424}
425
426impl Debug for CredentialProviderFunction {
427 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428 write!(
429 f,
430 "credential provider function at 0x{:016x}",
431 self.0.as_ref() as *const _ as *const () as usize
432 )
433 }
434}
435
436impl Eq for CredentialProviderFunction {}
437
438impl PartialEq for CredentialProviderFunction {
439 fn eq(&self, other: &Self) -> bool {
440 Arc::ptr_eq(&self.0, &other.0)
441 }
442}
443
444impl Hash for CredentialProviderFunction {
445 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
446 state.write_usize(Arc::as_ptr(&self.0) as *const () as usize)
447 }
448}
449
450#[cfg(feature = "serde")]
455#[derive(serde::Serialize, serde::Deserialize)]
456enum SerializedCredentialProvider {
457 #[cfg(feature = "python")]
458 #[serde(
459 serialize_with = "PythonObject::serialize_with_pyversion",
460 deserialize_with = "PythonObject::deserialize_with_pyversion"
461 )]
462 Builder(Arc<PythonObject>),
463 #[cfg(not(feature = "python"))]
464 Builder(RequiresPython),
465 #[cfg(feature = "python")]
466 #[serde(
467 serialize_with = "PythonObject::serialize_with_pyversion",
468 deserialize_with = "PythonObject::deserialize_with_pyversion"
469 )]
470 Provider(Arc<PythonObject>),
471 #[cfg(not(feature = "python"))]
472 Provider(RequiresPython),
473 AutoInit(AutoInitConfig),
474}
475
476#[cfg(all(feature = "serde", not(feature = "python")))]
477enum RequiresPython {}
478
479#[cfg(all(feature = "serde", not(feature = "python")))]
480impl serde::Serialize for RequiresPython {
481 fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
482 where
483 S: serde::Serializer,
484 {
485 match *self {}
486 }
487}
488
489#[cfg(all(feature = "serde", not(feature = "python")))]
490impl<'de> serde::Deserialize<'de> for RequiresPython {
491 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
492 where
493 D: serde::Deserializer<'de>,
494 {
495 use serde::de::Error;
496 Err(D::Error::custom(
497 "cannot deserialize Python credential provider without Python",
498 ))
499 }
500}
501
502#[cfg(feature = "serde")]
503impl<'de> serde::Deserialize<'de> for PlCredentialProvider {
504 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
505 where
506 D: serde::Deserializer<'de>,
507 {
508 Ok(
509 match SerializedCredentialProvider::deserialize(deserializer)? {
510 #[cfg(feature = "python")]
511 SerializedCredentialProvider::Builder(v) => {
512 Self::Python(PythonCredentialProvider::Builder(v))
513 },
514 #[cfg(feature = "python")]
515 SerializedCredentialProvider::Provider(v) => {
516 Self::Python(PythonCredentialProvider::Provider(v))
517 },
518 #[cfg(not(feature = "python"))]
519 SerializedCredentialProvider::Builder(v)
520 | SerializedCredentialProvider::Provider(v) => match v {},
521 SerializedCredentialProvider::AutoInit(v) => Self::AutoInit(v),
522 },
523 )
524 }
525}
526
527#[cfg(feature = "serde")]
528impl serde::Serialize for PlCredentialProvider {
529 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
530 where
531 S: serde::Serializer,
532 {
533 use serde::ser::Error;
534
535 let v = match self {
536 Self::Function(_) => {
537 return Err(S::Error::custom(format!("cannot serialize {self:?}")));
538 },
539 #[cfg(feature = "python")]
540 Self::Python(PythonCredentialProvider::Builder(v)) => {
541 SerializedCredentialProvider::Builder(v.clone())
542 },
543 #[cfg(feature = "python")]
544 Self::Python(PythonCredentialProvider::Provider(v)) => {
545 SerializedCredentialProvider::Provider(v.clone())
546 },
547 Self::AutoInit(v) => SerializedCredentialProvider::AutoInit(v.clone()),
548 };
549
550 v.serialize(serializer)
551 }
552}
553
554#[cfg(feature = "dsl-schema")]
555impl schemars::JsonSchema for PlCredentialProvider {
556 fn schema_name() -> std::borrow::Cow<'static, str> {
557 "PlCredentialProvider".into()
558 }
559
560 fn schema_id() -> std::borrow::Cow<'static, str> {
561 std::borrow::Cow::Borrowed(concat!(module_path!(), "::", "PlCredentialProvider"))
562 }
563
564 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
565 #[derive(schemars::JsonSchema)]
567 #[allow(dead_code)]
568 enum CredentialProviderSchema {
569 Builder(Vec<u8>),
570 Provider(Vec<u8>),
571 AutoInit(AutoInitConfig),
572 }
573
574 CredentialProviderSchema::json_schema(generator)
575 }
576}
577
578#[derive(Debug)]
580struct FetchedCredentialsCache<C>(tokio::sync::Mutex<(C, u64, bool)>);
581
582impl<C: Clone> FetchedCredentialsCache<C> {
583 fn new(init_creds: C) -> Self {
584 Self(tokio::sync::Mutex::new((init_creds, 0, true)))
585 }
586
587 async fn get_maybe_update(
588 &self,
589 update_func: impl Future<Output = PolarsResult<(C, u64)>>,
593 ) -> PolarsResult<C> {
594 let verbose = config::verbose();
595
596 fn expiry_msg(last_fetched_expiry: u64, now: u64) -> String {
597 if last_fetched_expiry == u64::MAX {
598 "expiry = (never expires)".into()
599 } else {
600 format!(
601 "expiry = {} (in {} seconds)",
602 last_fetched_expiry,
603 last_fetched_expiry.saturating_sub(now)
604 )
605 }
606 }
607
608 let mut inner = self.0.lock().await;
609 let (last_fetched_credentials, last_fetched_expiry, log_use_cached) = &mut *inner;
610
611 let current_time = SystemTime::now()
612 .duration_since(UNIX_EPOCH)
613 .unwrap()
614 .as_secs();
615
616 if *last_fetched_expiry <= current_time {
617 if verbose {
618 eprintln!(
619 "[FetchedCredentialsCache]: \
620 Call update_func: current_time = {}, \
621 last_fetched_expiry = {}",
622 current_time, *last_fetched_expiry
623 )
624 }
625
626 let (credentials, expiry) = update_func.await?;
627
628 *last_fetched_credentials = credentials;
629 *last_fetched_expiry = expiry;
630 *log_use_cached = true;
631
632 if expiry < current_time && expiry != 0 {
633 polars_bail!(
634 ComputeError:
635 "credential expiry time {} is older than system time {} \
636 by {} seconds",
637 expiry,
638 current_time,
639 current_time - expiry
640 )
641 }
642
643 if verbose {
644 eprintln!(
645 "[FetchedCredentialsCache]: Finish update_func: new {}",
646 expiry_msg(
647 *last_fetched_expiry,
648 SystemTime::now()
649 .duration_since(UNIX_EPOCH)
650 .unwrap()
651 .as_secs()
652 )
653 )
654 }
655 } else if verbose && *log_use_cached {
656 *log_use_cached = false;
657 let now = SystemTime::now()
658 .duration_since(UNIX_EPOCH)
659 .unwrap()
660 .as_secs();
661 eprintln!(
662 "[FetchedCredentialsCache]: Using cached credentials: \
663 current_time = {}, {}",
664 now,
665 expiry_msg(*last_fetched_expiry, now)
666 )
667 }
668
669 Ok(last_fetched_credentials.clone())
670 }
671}
672
673#[cfg(feature = "python")]
674mod python_impl {
675 use std::hash::Hash;
676 use std::sync::Arc;
677
678 use polars_error::{PolarsError, PolarsResult, polars_err, to_compute_err};
679 use polars_utils::pl_str::PlSmallStr;
680 use polars_utils::python_function::PythonObject;
681 use pyo3::exceptions::PyValueError;
682 use pyo3::pybacked::PyBackedStr;
683 use pyo3::types::{PyAnyMethods, PyDict, PyDictMethods};
684 use pyo3::{Python, intern};
685
686 use super::{AutoInitConfig, IntoCredentialProvider};
687
688 #[derive(Clone, Debug)]
689 pub enum PythonCredentialProvider {
690 Builder(Arc<PythonObject>),
692 Provider(Arc<PythonObject>),
694 }
695
696 impl PythonCredentialProvider {
697 pub(super) fn build_auto_init(
700 config: &AutoInitConfig,
701 clear_cached_credentials: bool,
702 ) -> PolarsResult<Option<Self>> {
703 let config_json = serde_json::to_string(config).map_err(to_compute_err)?;
704
705 let opt_initialized_py_object = Python::attach(|py| {
706 let v = py
707 .import(intern!(py, "polars.io.cloud.credential_provider._builder"))?
708 .call_method1(
709 intern!(py, "_build_auto_init_credential_provider"),
710 (config_json, clear_cached_credentials),
711 )?;
712
713 pyo3::PyResult::Ok((!v.is_none()).then(|| v.unbind()))
714 })?;
715
716 Ok(opt_initialized_py_object
717 .map(PythonObject)
718 .map(Arc::new)
719 .map(Self::Provider))
720 }
721
722 pub(super) fn try_into_initialized(
728 self,
729 clear_cached_credentials: bool,
730 ) -> PolarsResult<Option<Self>> {
731 match self {
732 Self::Builder(py_object) => {
733 let opt_initialized_py_object = Python::attach(|py| {
734 let build_fn =
735 py_object.getattr(py, intern!(py, "build_credential_provider"))?;
736
737 let v = build_fn.call1(py, (clear_cached_credentials,))?;
738 let v = (!v.is_none(py)).then_some(v);
739
740 pyo3::PyResult::Ok(v)
741 })?;
742
743 Ok(opt_initialized_py_object
744 .map(PythonObject)
745 .map(Arc::new)
746 .map(Self::Provider))
747 },
748 Self::Provider(_) => {
749 Ok(Some(self))
751 },
752 }
753 }
754
755 fn unwrap_as_provider(self) -> Arc<PythonObject> {
756 match self {
757 Self::Builder(_) => panic!(),
758 Self::Provider(v) => v,
759 }
760 }
761
762 pub(crate) fn unwrap_as_provider_ref(&self) -> &Arc<PythonObject> {
763 match self {
764 Self::Builder(_) => panic!(),
765 Self::Provider(v) => v,
766 }
767 }
768
769 pub(super) fn func_addr(&self) -> usize {
770 (match self {
771 Self::Builder(v) => Arc::as_ptr(v),
772 Self::Provider(v) => Arc::as_ptr(v),
773 }) as *const () as usize
774 }
775
776 pub fn stable_cache_key(&self) -> PolarsResult<Vec<u8>> {
777 let obj = match self {
778 Self::Builder(obj) | Self::Provider(obj) => obj,
779 };
780 let err = |e| {
781 polars_err!(ComputeError:
782 "failed to extract stable_cache_key: {e}")
783 };
784 Python::attach(|py| {
785 obj.call_method0(py, "stable_cache_key")
786 .and_then(|r| r.extract::<Vec<u8>>(py))
787 })
788 .map_err(err)
789 }
790 }
791
792 impl IntoCredentialProvider for PythonCredentialProvider {
793 #[cfg(feature = "aws")]
794 fn into_aws_provider(self) -> object_store::aws::AwsCredentialProvider {
795 use polars_error::PolarsResult;
796
797 use crate::cloud::credential_provider::{
798 CredentialProviderFunction, ObjectStoreCredential,
799 };
800
801 let func = self.unwrap_as_provider();
802
803 CredentialProviderFunction(Arc::new(move || {
804 let func = func.clone();
805 Box::pin(async move {
806 let mut credentials = object_store::aws::AwsCredential {
807 key_id: String::new(),
808 secret_key: String::new(),
809 token: None,
810 };
811
812 let expiry = Python::attach(|py| {
813 let v = func.0.call0(py)?.into_bound(py);
814 let (storage_options, expiry) =
815 v.extract::<(pyo3::Bound<'_, PyDict>, Option<u64>)>()?;
816
817 for (k, v) in storage_options.iter() {
818 let k = k.extract::<PyBackedStr>()?;
819 let v = v.extract::<Option<String>>()?;
820
821 match k.as_ref() {
822 "aws_access_key_id" => {
823 credentials.key_id = v.ok_or_else(|| {
824 PyValueError::new_err("aws_access_key_id was None")
825 })?;
826 },
827 "aws_secret_access_key" => {
828 credentials.secret_key = v.ok_or_else(|| {
829 PyValueError::new_err("aws_secret_access_key was None")
830 })?
831 },
832 "aws_session_token" => credentials.token = v,
833 v => {
834 return pyo3::PyResult::Err(PyValueError::new_err(format!(
835 "unknown configuration key for aws: {}, \
836 valid configuration keys are: \
837 {}, {}, {}",
838 v,
839 "aws_access_key_id",
840 "aws_secret_access_key",
841 "aws_session_token"
842 )));
843 },
844 }
845 }
846
847 pyo3::PyResult::Ok(expiry.unwrap_or(u64::MAX))
848 })?;
849
850 if credentials.key_id.is_empty() {
851 return Err(PolarsError::ComputeError(
852 "aws_access_key_id was empty or not given".into(),
853 ));
854 }
855
856 if credentials.secret_key.is_empty() {
857 return Err(PolarsError::ComputeError(
858 "aws_secret_access_key was empty or not given".into(),
859 ));
860 }
861
862 PolarsResult::Ok((ObjectStoreCredential::Aws(Arc::new(credentials)), expiry))
863 })
864 }))
865 .into_aws_provider()
866 }
867
868 #[cfg(feature = "azure")]
869 fn into_azure_provider(self) -> object_store::azure::AzureCredentialProvider {
870 use object_store::azure::AzureAccessKey;
871 use percent_encoding::percent_decode_str;
872 use polars_core::config::verbose_print_sensitive;
873 use polars_error::PolarsResult;
874
875 use crate::cloud::credential_provider::{
876 CredentialProviderFunction, ObjectStoreCredential,
877 };
878
879 let func = self.unwrap_as_provider();
880
881 return CredentialProviderFunction(Arc::new(move || {
882 let func = func.clone();
883 Box::pin(async move {
884 let mut credentials = None;
885
886 static VALID_KEYS_MSG: &str =
887 "valid configuration keys are: ('account_key', 'bearer_token', 'sas_token')";
888
889 let expiry = Python::attach(|py| {
890 let v = func.0.call0(py)?.into_bound(py);
891 let (storage_options, expiry) =
892 v.extract::<(pyo3::Bound<'_, PyDict>, Option<u64>)>()?;
893
894 for (k, v) in storage_options.iter() {
895 let k = k.extract::<PyBackedStr>()?;
896 let v = v.extract::<String>()?;
897
898 match k.as_ref() {
899 "account_key" => {
900 credentials =
901 Some(object_store::azure::AzureCredential::AccessKey(
902 AzureAccessKey::try_new(v.as_str()).map_err(|e| {
903 PyValueError::new_err(e.to_string())
904 })?,
905 ))
906 },
907 "bearer_token" => {
908 credentials =
909 Some(object_store::azure::AzureCredential::BearerToken(v))
910 },
911 "sas_token" => {
912 credentials =
913 Some(object_store::azure::AzureCredential::SASToken(
914 split_sas(&v).map_err(|err_msg| {
915 verbose_print_sensitive(|| {
916 format!("error decoding SAS token: {err_msg} (token: {v})")
917 });
918
919 PyValueError::new_err(format!(
920 "error decoding SAS token: {err_msg}. \
921 Set POLARS_VERBOSE_SENSITIVE=1 to print the value"
922 ))
923 })?,
924 ))
925 },
926 v => {
927 return pyo3::PyResult::Err(PyValueError::new_err(format!(
928 "unknown configuration key for azure: {v}, {VALID_KEYS_MSG}"
929 )));
930 },
931 }
932 }
933
934 pyo3::PyResult::Ok(expiry.unwrap_or(u64::MAX))
935 })?;
936
937 let Some(credentials) = credentials else {
938 return Err(PolarsError::ComputeError(
939 format!(
940 "did not find a valid configuration key for azure, {VALID_KEYS_MSG}"
941 )
942 .into(),
943 ));
944 };
945
946 PolarsResult::Ok((ObjectStoreCredential::Azure(Arc::new(credentials)), expiry))
947 })
948 }))
949 .into_azure_provider();
950
951 fn split_sas(sas: &str) -> Result<Vec<(String, String)>, &'static str> {
955 let sas = percent_decode_str(sas)
956 .decode_utf8()
957 .map_err(|_| "UTF-8 decode error")?;
958
959 let kv_str_pairs = sas
960 .trim_start_matches('?')
961 .split('&')
962 .filter(|s| !s.chars().all(char::is_whitespace));
963
964 let mut pairs = Vec::new();
965
966 for kv_pair_str in kv_str_pairs {
967 let (k, v) = kv_pair_str
968 .trim()
969 .split_once('=')
970 .ok_or("missing SAS component")?;
971 pairs.push((k.into(), v.into()))
972 }
973
974 Ok(pairs)
975 }
976 }
977
978 #[cfg(feature = "gcp")]
979 fn into_gcp_provider(self) -> object_store::gcp::GcpCredentialProvider {
980 use polars_error::PolarsResult;
981
982 use crate::cloud::credential_provider::{
983 CredentialProviderFunction, ObjectStoreCredential,
984 };
985
986 let func = self.unwrap_as_provider();
987
988 CredentialProviderFunction(Arc::new(move || {
989 let func = func.clone();
990 Box::pin(async move {
991 let mut credentials = object_store::gcp::GcpCredential {
992 bearer: String::new(),
993 };
994
995 let expiry = Python::attach(|py| {
996 let v = func.0.call0(py)?.into_bound(py);
997 let (storage_options, expiry) =
998 v.extract::<(pyo3::Bound<'_, PyDict>, Option<u64>)>()?;
999
1000 for (k, v) in storage_options.iter() {
1001 let k = k.extract::<PyBackedStr>()?;
1002 let v = v.extract::<String>()?;
1003
1004 match k.as_ref() {
1005 "bearer_token" => credentials.bearer = v,
1006 v => {
1007 return pyo3::PyResult::Err(PyValueError::new_err(format!(
1008 "unknown configuration key for gcp: {}, \
1009 valid configuration keys are: {}",
1010 v, "bearer_token",
1011 )));
1012 },
1013 }
1014 }
1015
1016 pyo3::PyResult::Ok(expiry.unwrap_or(u64::MAX))
1017 })?;
1018
1019 if credentials.bearer.is_empty() {
1020 return Err(PolarsError::ComputeError(
1021 "bearer was empty or not given".into(),
1022 ));
1023 }
1024
1025 PolarsResult::Ok((ObjectStoreCredential::Gcp(Arc::new(credentials)), expiry))
1026 })
1027 }))
1028 .into_gcp_provider()
1029 }
1030
1031 fn storage_update_options(&self) -> PolarsResult<Vec<(PlSmallStr, PlSmallStr)>> {
1034 let py_object = self.unwrap_as_provider_ref();
1035
1036 Python::attach(|py| {
1037 py_object
1038 .getattr(py, "_storage_update_options")
1039 .map_or(Ok(vec![]), |f| {
1040 let v = f
1041 .call0(py)?
1042 .extract::<pyo3::Bound<'_, PyDict>>(py)
1043 .map_err(pyo3::PyErr::from)?;
1044
1045 let mut out = Vec::with_capacity(v.len());
1046
1047 for dict_item in v.call_method0("items")?.try_iter()? {
1048 let (key, value) =
1049 dict_item?.extract::<(PyBackedStr, PyBackedStr)>()?;
1050
1051 out.push(((&*key).into(), (&*value).into()))
1052 }
1053
1054 Ok(out)
1055 })
1056 })
1057 }
1058 }
1059
1060 impl Eq for PythonCredentialProvider {}
1064
1065 impl PartialEq for PythonCredentialProvider {
1066 fn eq(&self, other: &Self) -> bool {
1067 self.func_addr() == other.func_addr()
1068 }
1069 }
1070
1071 impl Hash for PythonCredentialProvider {
1072 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1073 state.write_usize(self.func_addr())
1078 }
1079 }
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 #[cfg(feature = "serde")]
1085 #[allow(clippy::redundant_pattern_matching)]
1086 #[test]
1087 fn test_serde() {
1088 use super::*;
1089
1090 assert!(matches!(
1091 serde_json::to_string(&Some(PlCredentialProvider::from_func(|| {
1092 Box::pin(core::future::ready(PolarsResult::Ok((
1093 ObjectStoreCredential::None,
1094 0,
1095 ))))
1096 }))),
1097 Err(_)
1098 ));
1099
1100 assert!(matches!(
1101 serde_json::to_string(&Option::<PlCredentialProvider>::None),
1102 Ok(String { .. })
1103 ));
1104
1105 assert!(matches!(
1106 serde_json::from_str::<Option<PlCredentialProvider>>(
1107 serde_json::to_string(&Option::<PlCredentialProvider>::None)
1108 .unwrap()
1109 .as_str()
1110 ),
1111 Ok(None)
1112 ));
1113 }
1114
1115 #[cfg(feature = "serde")]
1116 #[test]
1117 fn test_serde_auto_init() {
1118 use super::*;
1119
1120 let provider = PlCredentialProvider::AutoInit(AutoInitConfig::Aws {
1121 profile_name: Some("p".into()),
1122 region_name: None,
1123 auto_init_unhandled_key: None,
1124 storage_options: [("region".into(), Some("eu-west-1".into()))]
1125 .into_iter()
1126 .collect(),
1127 });
1128
1129 let mut bytes = vec![];
1130 polars_utils::pl_serialize::serialize_dsl(&mut bytes, &provider).unwrap();
1131 let out: PlCredentialProvider =
1132 polars_utils::pl_serialize::deserialize_dsl(&mut bytes.as_slice()).unwrap();
1133 assert_eq!(out, provider);
1134
1135 let out: PlCredentialProvider =
1136 serde_json::from_str(&serde_json::to_string(&provider).unwrap()).unwrap();
1137 assert_eq!(out, provider);
1138
1139 let PlCredentialProvider::AutoInit(config) = &provider else {
1141 unreachable!()
1142 };
1143 assert_eq!(
1144 serde_json::to_string(config).unwrap(),
1145 r#"{"aws":{"profile_name":"p","region_name":null,"auto_init_unhandled_key":null,"storage_options":{"region":"eu-west-1"}}}"#
1146 );
1147 assert_eq!(
1148 serde_json::from_str::<AutoInitConfig>(r#"{"gcp":{}}"#).unwrap(),
1149 AutoInitConfig::Gcp {}
1150 );
1151 }
1152
1153 #[cfg(feature = "gcp")]
1154 #[test]
1155 fn test_auto_init_gcp_token_is_native() {
1156 use super::*;
1157
1158 let provider =
1159 PlCredentialProvider::AutoInit(AutoInitConfig::GcpToken { token: "A".into() })
1160 .try_into_initialized(false)
1161 .unwrap()
1162 .unwrap();
1163
1164 let PlCredentialProvider::Function(CredentialProviderFunction(f)) = provider else {
1165 panic!()
1166 };
1167 let (credential, expiry) = futures::executor::block_on(f()).unwrap();
1168 assert_eq!(credential.unwrap_gcp().bearer, "A");
1169 assert_eq!(expiry, u64::MAX);
1170 }
1171}