Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
pola-rs
GitHub Repository: pola-rs/polars
Path: blob/main/py-polars/tests/unit/io/cloud/test_credential_provider.py
6939 views
1
import io
2
import pickle
3
import sys
4
from datetime import datetime, timezone
5
from pathlib import Path
6
from typing import Any
7
from unittest.mock import Mock
8
9
import pytest
10
11
import polars as pl
12
import polars.io.cloud.credential_provider
13
from polars._typing import PartitioningScheme
14
from polars.io.cloud._utils import NoPickleOption
15
from polars.io.cloud.credential_provider._builder import (
16
AutoInit,
17
CredentialProviderBuilder,
18
_init_credential_provider_builder,
19
)
20
from polars.io.cloud.credential_provider._providers import (
21
CachedCredentialProvider,
22
CachingCredentialProvider,
23
UserProvidedGCPToken,
24
)
25
26
27
@pytest.mark.parametrize(
28
"io_func",
29
[
30
*[pl.scan_parquet, pl.read_parquet],
31
pl.scan_csv,
32
*[pl.scan_ndjson, pl.read_ndjson],
33
pl.scan_ipc,
34
],
35
)
36
def test_credential_provider_scan(
37
io_func: Any, monkeypatch: pytest.MonkeyPatch
38
) -> None:
39
err_magic = "err_magic_3"
40
41
def raises(*_: None, **__: None) -> None:
42
raise AssertionError(err_magic)
43
44
from polars.io.cloud.credential_provider._builder import CredentialProviderBuilder
45
46
monkeypatch.setattr(CredentialProviderBuilder, "__init__", raises)
47
48
with pytest.raises(AssertionError, match=err_magic):
49
io_func("s3://bucket/path", credential_provider="auto")
50
51
with pytest.raises(AssertionError, match=err_magic):
52
io_func(
53
"s3://bucket/path",
54
credential_provider="auto",
55
storage_options={"aws_region": "eu-west-1"},
56
)
57
58
# We can't test these with the `read_` functions as they end up executing
59
# the query
60
if io_func.__name__.startswith("scan_"):
61
# Passing `None` should disable the automatic instantiation of
62
# `CredentialProviderAWS`
63
io_func("s3://bucket/path", credential_provider=None)
64
65
err_magic = "err_magic_7"
66
67
def raises_2() -> pl.CredentialProviderFunctionReturn:
68
raise AssertionError(err_magic)
69
70
with pytest.raises(AssertionError, match=err_magic):
71
io_func("s3://bucket/path", credential_provider=raises_2).collect()
72
73
74
@pytest.mark.parametrize(
75
("provider_class", "path"),
76
[
77
(polars.io.cloud.credential_provider.CredentialProviderAWS, "s3://.../..."),
78
(polars.io.cloud.credential_provider.CredentialProviderGCP, "gs://.../..."),
79
(polars.io.cloud.credential_provider.CredentialProviderAzure, "az://.../..."),
80
],
81
)
82
def test_credential_provider_serialization_auto_init(
83
provider_class: polars.io.cloud.credential_provider.CredentialProvider,
84
path: str,
85
monkeypatch: pytest.MonkeyPatch,
86
) -> None:
87
def raises_1(*a: Any, **kw: Any) -> None:
88
msg = "err_magic_1"
89
raise AssertionError(msg)
90
91
monkeypatch.setattr(provider_class, "__init__", raises_1)
92
93
# If this is not set we will get an error before hitting the credential
94
# provider logic when polars attempts to retrieve the region from AWS.
95
monkeypatch.setenv("AWS_REGION", "eu-west-1")
96
97
# Credential provider should not be initialized during query plan construction.
98
q = pl.scan_parquet(path)
99
100
# Check baseline - query plan is configured to auto-initialize the credential
101
# provider.
102
with pytest.raises(AssertionError, match="err_magic_1"):
103
q.collect()
104
105
q = pickle.loads(pickle.dumps(q))
106
107
def raises_2(*a: Any, **kw: Any) -> None:
108
msg = "err_magic_2"
109
raise AssertionError(msg)
110
111
monkeypatch.setattr(provider_class, "__init__", raises_2)
112
113
# Check that auto-initialization happens upon executing the deserialized
114
# query.
115
with pytest.raises(AssertionError, match="err_magic_2"):
116
q.collect()
117
118
119
def test_credential_provider_serialization_custom_provider() -> None:
120
err_magic = "err_magic_3"
121
122
class ErrCredentialProvider(pl.CredentialProvider):
123
def __call__(self) -> pl.CredentialProviderFunctionReturn:
124
raise AssertionError(err_magic)
125
126
lf = pl.scan_parquet(
127
"s3://bucket/path", credential_provider=ErrCredentialProvider()
128
)
129
130
serialized = lf.serialize()
131
132
lf = pl.LazyFrame.deserialize(io.BytesIO(serialized))
133
134
with pytest.raises(AssertionError, match=err_magic):
135
lf.collect()
136
137
138
def test_credential_provider_gcp_skips_config_autoload(
139
monkeypatch: pytest.MonkeyPatch,
140
) -> None:
141
monkeypatch.setenv("GOOGLE_SERVICE_ACCOUNT_PATH", "__non_existent")
142
143
with pytest.raises(OSError, match="__non_existent"):
144
pl.scan_parquet("gs://.../...", credential_provider=None).collect()
145
146
err_magic = "err_magic_3"
147
148
def raises() -> pl.CredentialProviderFunctionReturn:
149
raise AssertionError(err_magic)
150
151
with pytest.raises(AssertionError, match=err_magic):
152
pl.scan_parquet("gs://.../...", credential_provider=raises).collect()
153
154
155
def test_credential_provider_aws_import_error_with_requested_profile(
156
monkeypatch: pytest.MonkeyPatch,
157
) -> None:
158
def _session(self: Any) -> None:
159
msg = "err_magic_3"
160
raise ImportError(msg)
161
162
monkeypatch.setattr(pl.CredentialProviderAWS, "_session", _session)
163
monkeypatch.setenv("AWS_REGION", "eu-west-1")
164
165
q = pl.scan_parquet(
166
"s3://.../...",
167
credential_provider=pl.CredentialProviderAWS(profile_name="test_profile"),
168
)
169
170
with pytest.raises(
171
pl.exceptions.ComputeError,
172
match=(
173
"cannot load requested aws_profile 'test_profile': ImportError: err_magic_3"
174
),
175
):
176
q.collect()
177
178
q = pl.scan_parquet(
179
"s3://.../...",
180
storage_options={"aws_profile": "test_profile"},
181
)
182
183
with pytest.raises(
184
pl.exceptions.ComputeError,
185
match=(
186
"cannot load requested aws_profile 'test_profile': ImportError: err_magic_3"
187
),
188
):
189
q.collect()
190
191
192
@pytest.mark.slow
193
@pytest.mark.write_disk
194
def test_credential_provider_aws_endpoint_url_scan_no_parameters(
195
tmp_path: Path,
196
monkeypatch: pytest.MonkeyPatch,
197
capfd: pytest.CaptureFixture[str],
198
) -> None:
199
tmp_path.mkdir(exist_ok=True)
200
201
_set_default_credentials(tmp_path, monkeypatch)
202
cfg_file_path = tmp_path / "config"
203
204
monkeypatch.setenv("AWS_CONFIG_FILE", str(cfg_file_path))
205
monkeypatch.setenv("POLARS_VERBOSE", "1")
206
207
cfg_file_path.write_text("""\
208
[default]
209
endpoint_url = http://localhost:333
210
""")
211
212
# Scan with no parameters should load via CredentialProviderAWS
213
q = pl.scan_parquet("s3://.../...")
214
215
capfd.readouterr()
216
217
with pytest.raises(IOError, match=r"Error performing HEAD http://localhost:333"):
218
q.collect()
219
220
capture = capfd.readouterr().err
221
lines = capture.splitlines()
222
223
assert "[CredentialProviderAWS]: Loaded endpoint_url: http://localhost:333" in lines
224
225
226
@pytest.mark.slow
227
@pytest.mark.write_disk
228
def test_credential_provider_aws_endpoint_url_serde(
229
tmp_path: Path,
230
monkeypatch: pytest.MonkeyPatch,
231
capfd: pytest.CaptureFixture[str],
232
) -> None:
233
tmp_path.mkdir(exist_ok=True)
234
235
_set_default_credentials(tmp_path, monkeypatch)
236
cfg_file_path = tmp_path / "config"
237
238
monkeypatch.setenv("AWS_CONFIG_FILE", str(cfg_file_path))
239
monkeypatch.setenv("POLARS_VERBOSE", "1")
240
241
cfg_file_path.write_text("""\
242
[default]
243
endpoint_url = http://localhost:333
244
""")
245
246
q = pl.scan_parquet("s3://.../...")
247
q = pickle.loads(pickle.dumps(q))
248
249
cfg_file_path.write_text("""\
250
[default]
251
endpoint_url = http://localhost:777
252
""")
253
254
capfd.readouterr()
255
256
with pytest.raises(IOError, match=r"Error performing HEAD http://localhost:777"):
257
q.collect()
258
259
260
@pytest.mark.slow
261
@pytest.mark.write_disk
262
def test_credential_provider_aws_endpoint_url_with_storage_options(
263
tmp_path: Path,
264
monkeypatch: pytest.MonkeyPatch,
265
capfd: pytest.CaptureFixture[str],
266
) -> None:
267
tmp_path.mkdir(exist_ok=True)
268
269
_set_default_credentials(tmp_path, monkeypatch)
270
cfg_file_path = tmp_path / "config"
271
272
monkeypatch.setenv("AWS_CONFIG_FILE", str(cfg_file_path))
273
monkeypatch.setenv("POLARS_VERBOSE", "1")
274
275
cfg_file_path.write_text("""\
276
[default]
277
endpoint_url = http://localhost:333
278
""")
279
280
# Previously we would not initialize a credential provider at all if secrets
281
# were given under `storage_options`. Now we always initialize so that we
282
# can load the `endpoint_url`, but we decide at the very last second whether
283
# to also retrieve secrets using the credential provider.
284
q = pl.scan_parquet(
285
"s3://.../...",
286
storage_options={
287
"aws_access_key_id": "...",
288
"aws_secret_access_key": "...",
289
},
290
)
291
292
with pytest.raises(IOError, match=r"Error performing HEAD http://localhost:333"):
293
q.collect()
294
295
capture = capfd.readouterr().err
296
lines = capture.splitlines()
297
298
assert (
299
"[CredentialProviderAWS]: Will not be used as a provider: unhandled key "
300
"in storage_options: 'aws_secret_access_key'"
301
) in lines
302
assert "[CredentialProviderAWS]: Loaded endpoint_url: http://localhost:333" in lines
303
304
305
@pytest.mark.parametrize(
306
"storage_options",
307
[
308
{"aws_endpoint_url": "http://localhost:777"},
309
{
310
"aws_access_key_id": "...",
311
"aws_secret_access_key": "...",
312
"aws_endpoint_url": "http://localhost:777",
313
},
314
],
315
)
316
@pytest.mark.slow
317
@pytest.mark.write_disk
318
def test_credential_provider_aws_endpoint_url_passed_in_storage_options(
319
storage_options: dict[str, str],
320
tmp_path: Path,
321
monkeypatch: pytest.MonkeyPatch,
322
) -> None:
323
tmp_path.mkdir(exist_ok=True)
324
325
_set_default_credentials(tmp_path, monkeypatch)
326
cfg_file_path = tmp_path / "config"
327
monkeypatch.setenv("AWS_CONFIG_FILE", str(cfg_file_path))
328
329
cfg_file_path.write_text("""\
330
[default]
331
endpoint_url = http://localhost:333
332
""")
333
334
q = pl.scan_parquet("s3://.../...")
335
336
with pytest.raises(IOError, match=r"Error performing HEAD http://localhost:333"):
337
q.collect()
338
339
# An endpoint_url passed in `storage_options` should take precedence.
340
q = pl.scan_parquet(
341
"s3://.../...",
342
storage_options=storage_options,
343
)
344
345
with pytest.raises(IOError, match=r"Error performing HEAD http://localhost:777"):
346
q.collect()
347
348
349
def _set_default_credentials(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
350
creds_file_path = tmp_path / "credentials"
351
352
creds_file_path.write_text("""\
353
[default]
354
aws_access_key_id=Z
355
aws_secret_access_key=Z
356
""")
357
358
monkeypatch.setenv("AWS_SHARED_CREDENTIALS_FILE", str(creds_file_path))
359
360
361
@pytest.mark.slow
362
def test_credential_provider_python_builder_cache(
363
monkeypatch: pytest.MonkeyPatch,
364
capfd: pytest.CaptureFixture[str],
365
) -> None:
366
# Tests caching of building credential providers.
367
def dummy_static_aws_credentials(*a: Any, **kw: Any) -> Any:
368
return {
369
"aws_access_key_id": "...",
370
"aws_secret_access_key": "...",
371
}, None
372
373
with monkeypatch.context() as cx:
374
provider_init = Mock(wraps=pl.CredentialProviderAWS.__init__)
375
376
cx.setattr(
377
pl.CredentialProviderAWS,
378
"__init__",
379
lambda *a, **kw: provider_init(*a, **kw),
380
)
381
382
cx.setattr(
383
pl.CredentialProviderAWS,
384
"retrieve_credentials_impl",
385
dummy_static_aws_credentials,
386
)
387
388
# Ensure we are building a new query every time.
389
def get_q() -> pl.LazyFrame:
390
return pl.scan_parquet(
391
"s3://.../...",
392
storage_options={
393
"aws_profile": "A",
394
"aws_endpoint_url": "http://localhost",
395
},
396
credential_provider="auto",
397
)
398
399
assert provider_init.call_count == 0
400
401
with pytest.raises(OSError):
402
get_q().collect()
403
404
assert provider_init.call_count == 1
405
406
with pytest.raises(OSError):
407
get_q().collect()
408
409
assert provider_init.call_count == 1
410
411
with pytest.raises(OSError):
412
pl.scan_parquet(
413
"s3://.../...",
414
storage_options={
415
"aws_profile": "B",
416
"aws_endpoint_url": "http://localhost",
417
},
418
credential_provider="auto",
419
).collect()
420
421
assert provider_init.call_count == 2
422
423
with pytest.raises(OSError):
424
get_q().collect()
425
426
assert provider_init.call_count == 2
427
428
cx.setenv("POLARS_CREDENTIAL_PROVIDER_BUILDER_CACHE_SIZE", "0")
429
430
with pytest.raises(OSError):
431
get_q().collect()
432
433
# Note: Increments by 2 due to Rust-side object store rebuilding.
434
435
assert provider_init.call_count == 4
436
437
with pytest.raises(OSError):
438
get_q().collect()
439
440
assert provider_init.call_count == 6
441
442
with monkeypatch.context() as cx:
443
cx.setenv("POLARS_VERBOSE", "1")
444
builder = _init_credential_provider_builder(
445
"auto",
446
"s3://.../...",
447
None,
448
"test",
449
)
450
assert builder is not None
451
452
capfd.readouterr()
453
454
builder.build_credential_provider()
455
builder.build_credential_provider()
456
457
capture = capfd.readouterr().err
458
459
# Ensure cache key is memoized on generation
460
assert capture.count("AutoInit cache key") == 1
461
462
pickle.loads(pickle.dumps(builder)).build_credential_provider()
463
464
capture = capfd.readouterr().err
465
466
# Ensure cache key is not serialized
467
assert capture.count("AutoInit cache key") == 1
468
469
470
@pytest.mark.slow
471
def test_credential_provider_python_credentials_cache(
472
monkeypatch: pytest.MonkeyPatch,
473
) -> None:
474
credentials_func = Mock(
475
wraps=lambda: (
476
{
477
"aws_access_key_id": "...",
478
"aws_secret_access_key": "...",
479
},
480
None,
481
)
482
)
483
484
monkeypatch.setattr(
485
pl.CredentialProviderAWS,
486
"retrieve_credentials_impl",
487
credentials_func,
488
)
489
490
assert credentials_func.call_count == 0
491
492
provider = pl.CredentialProviderAWS()
493
494
provider()
495
assert credentials_func.call_count == 1
496
497
provider()
498
assert credentials_func.call_count == 1
499
500
monkeypatch.setenv("POLARS_DISABLE_PYTHON_CREDENTIAL_CACHING", "1")
501
502
provider()
503
assert credentials_func.call_count == 2
504
505
provider()
506
assert credentials_func.call_count == 3
507
508
monkeypatch.delenv("POLARS_DISABLE_PYTHON_CREDENTIAL_CACHING")
509
510
provider()
511
assert credentials_func.call_count == 4
512
513
provider()
514
assert credentials_func.call_count == 4
515
516
assert provider._cached_credentials.get() is not None
517
assert pickle.loads(pickle.dumps(provider))._cached_credentials.get() is None
518
519
assert provider() == (
520
{
521
"aws_access_key_id": "...",
522
"aws_secret_access_key": "...",
523
},
524
None,
525
)
526
527
provider()[0]["A"] = "A"
528
529
assert provider() == (
530
{
531
"aws_access_key_id": "...",
532
"aws_secret_access_key": "...",
533
},
534
None,
535
)
536
537
538
def test_no_pickle_option() -> None:
539
v = NoPickleOption(3)
540
assert v.get() == 3
541
542
out = pickle.loads(pickle.dumps(v))
543
544
assert out.get() is None
545
546
547
@pytest.mark.write_disk
548
def test_credential_provider_aws_expiry(
549
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
550
) -> None:
551
credential_file_path = tmp_path / "credentials.json"
552
553
credential_file_path.write_text(
554
"""\
555
{
556
"Version": 1,
557
"AccessKeyId": "123",
558
"SecretAccessKey": "456",
559
"SessionToken": "789",
560
"Expiration": "2099-01-01T00:00:00+00:00"
561
}
562
"""
563
)
564
565
cfg_file_path = tmp_path / "config"
566
567
credential_file_path_str = str(credential_file_path).replace("\\", "/")
568
569
cfg_file_path.write_text(f"""\
570
[profile cred_process]
571
credential_process = "{sys.executable}" -c "from pathlib import Path; print(Path('{credential_file_path_str}').read_text())"
572
""")
573
574
monkeypatch.setenv("AWS_CONFIG_FILE", str(cfg_file_path))
575
576
creds, expiry = pl.CredentialProviderAWS(profile_name="cred_process")()
577
578
assert creds == {
579
"aws_access_key_id": "123",
580
"aws_secret_access_key": "456",
581
"aws_session_token": "789",
582
}
583
584
assert expiry is not None
585
586
assert datetime.fromtimestamp(expiry, tz=timezone.utc) == datetime.fromisoformat(
587
"2099-01-01T00:00:00+00:00"
588
)
589
590
credential_file_path.write_text(
591
"""\
592
{
593
"Version": 1,
594
"AccessKeyId": "...",
595
"SecretAccessKey": "...",
596
"SessionToken": "..."
597
}
598
"""
599
)
600
601
creds, expiry = pl.CredentialProviderAWS(profile_name="cred_process")()
602
603
assert creds == {
604
"aws_access_key_id": "...",
605
"aws_secret_access_key": "...",
606
"aws_session_token": "...",
607
}
608
609
assert expiry is None
610
611
612
@pytest.mark.slow
613
@pytest.mark.parametrize(
614
(
615
"credential_provider_class",
616
"scan_path",
617
"initial_credentials",
618
"updated_credentials",
619
),
620
[
621
(
622
pl.CredentialProviderAWS,
623
"s3://.../...",
624
{"aws_access_key_id": "initial", "aws_secret_access_key": "initial"},
625
{"aws_access_key_id": "updated", "aws_secret_access_key": "updated"},
626
),
627
(
628
pl.CredentialProviderAzure,
629
"abfss://container@storage_account.dfs.core.windows.net/bucket",
630
{"bearer_token": "initial"},
631
{"bearer_token": "updated"},
632
),
633
(
634
pl.CredentialProviderGCP,
635
"gs://.../...",
636
{"bearer_token": "initial"},
637
{"bearer_token": "updated"},
638
),
639
],
640
)
641
def test_credential_provider_rebuild_clears_cache(
642
credential_provider_class: type[CachingCredentialProvider],
643
scan_path: str,
644
initial_credentials: dict[str, str],
645
updated_credentials: dict[str, str],
646
monkeypatch: pytest.MonkeyPatch,
647
) -> None:
648
assert initial_credentials != updated_credentials
649
650
monkeypatch.setattr(
651
credential_provider_class,
652
"retrieve_credentials_impl",
653
lambda *_: (initial_credentials, None),
654
)
655
656
storage_options = (
657
{"aws_endpoint_url": "http://localhost:333"}
658
if credential_provider_class == pl.CredentialProviderAWS
659
else None
660
)
661
662
builder = _init_credential_provider_builder(
663
"auto",
664
scan_path,
665
storage_options=storage_options,
666
caller_name="test",
667
)
668
669
assert builder is not None
670
671
# This is a separate one for testing local to this function.
672
provider_local = credential_provider_class()
673
674
# Set the cache
675
provider_local()
676
677
# Now update the the retrieval function to return updated credentials.
678
monkeypatch.setattr(
679
credential_provider_class,
680
"retrieve_credentials_impl",
681
lambda *_: (updated_credentials, None),
682
)
683
684
# Despite "retrieve_credentials_impl" being updated, the providers should
685
# still return the initial credentials, as they were cached with an expiry
686
# of None.
687
assert provider_local() == (initial_credentials, None)
688
689
q = pl.scan_parquet(
690
scan_path,
691
storage_options=storage_options,
692
credential_provider="auto",
693
)
694
695
with pytest.raises(OSError):
696
q.collect()
697
698
provider_at_scan = builder.build_credential_provider()
699
700
assert provider_at_scan is not None
701
assert provider_at_scan() == (updated_credentials, None)
702
703
assert provider_local() == (initial_credentials, None)
704
705
provider_local.clear_cached_credentials()
706
707
assert provider_local() == (updated_credentials, None)
708
709
710
def test_user_gcp_token_provider(
711
monkeypatch: pytest.MonkeyPatch,
712
) -> None:
713
provider = UserProvidedGCPToken("A")
714
assert provider() == ({"bearer_token": "A"}, None)
715
monkeypatch.setenv("POLARS_DISABLE_PYTHON_CREDENTIAL_CACHING", "1")
716
assert provider() == ({"bearer_token": "A"}, None)
717
718
719
def test_auto_init_cache_key_memoize(monkeypatch: pytest.MonkeyPatch) -> None:
720
get_cache_key_impl = Mock(wraps=AutoInit.get_cache_key_impl)
721
monkeypatch.setattr(
722
AutoInit,
723
"get_cache_key_impl",
724
lambda *a, **kw: get_cache_key_impl(*a, **kw),
725
)
726
727
v = AutoInit(int)
728
729
assert get_cache_key_impl.call_count == 0
730
731
v.get_or_init_cache_key()
732
assert get_cache_key_impl.call_count == 1
733
734
v.get_or_init_cache_key()
735
assert get_cache_key_impl.call_count == 1
736
737
738
def test_cached_credential_provider_returns_copied_creds() -> None:
739
provider_func = Mock(wraps=lambda: ({"A": "A"}, None))
740
provider = CachedCredentialProvider(provider_func)
741
742
assert provider_func.call_count == 0
743
744
provider()
745
assert provider() == ({"A": "A"}, None)
746
747
assert provider_func.call_count == 1
748
749
provider()[0]["B"] = "B"
750
751
assert provider() == ({"A": "A"}, None)
752
753
assert provider_func.call_count == 1
754
755
756
@pytest.mark.parametrize(
757
"partition_target",
758
[
759
pl.PartitionByKey("s3://.../...", by=""),
760
pl.PartitionMaxSize("s3://.../...", max_size=1),
761
pl.PartitionParted("s3://.../...", by=""),
762
],
763
)
764
def test_credential_provider_init_from_partition_target(
765
partition_target: PartitioningScheme,
766
) -> None:
767
assert isinstance(
768
_init_credential_provider_builder(
769
"auto",
770
partition_target,
771
None,
772
"test",
773
),
774
CredentialProviderBuilder,
775
)
776
777
778
@pytest.mark.slow
779
def test_cache_user_credential_provider(monkeypatch: pytest.MonkeyPatch) -> None:
780
user_provider = Mock(
781
return_value=(
782
{"aws_access_key_id": "...", "aws_secret_access_key": "..."},
783
None,
784
)
785
)
786
787
def get_q() -> pl.LazyFrame:
788
return pl.scan_parquet(
789
"s3://.../...",
790
storage_options={"aws_endpoint_url": "http://localhost:333"},
791
credential_provider=user_provider,
792
)
793
794
assert user_provider.call_count == 0
795
796
with pytest.raises(OSError, match="http://localhost:333"):
797
get_q().collect()
798
799
assert user_provider.call_count == 2
800
801
with pytest.raises(OSError, match="http://localhost:333"):
802
get_q().collect()
803
804
assert user_provider.call_count == 3
805
806
monkeypatch.setenv("POLARS_CREDENTIAL_PROVIDER_BUILDER_CACHE_SIZE", "0")
807
808
with pytest.raises(OSError, match="http://localhost:333"):
809
get_q().collect()
810
811
assert user_provider.call_count == 5
812
813