Skip to content

Commit 92fc2b0

Browse files
authored
feat: add support for partial list buckets (#1606)
Add support for partial list buckets
1 parent 195d644 commit 92fc2b0

File tree

2 files changed

+88
-8
lines changed

2 files changed

+88
-8
lines changed

google/cloud/storage/client.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@
6464
_marker = object()
6565

6666

67+
def _buckets_page_start(iterator, page, response):
68+
"""Grab unreachable buckets after a :class:`~google.cloud.iterator.Page` started."""
69+
unreachable = response.get("unreachable", [])
70+
if not isinstance(unreachable, list):
71+
raise TypeError(
72+
f"expected unreachable to be list, but obtained {type(unreachable)}"
73+
)
74+
page.unreachable = unreachable
75+
6776
class Client(ClientWithProject):
6877
"""Client to bundle configuration needed for API requests.
6978
@@ -1458,6 +1467,7 @@ def list_buckets(
14581467
retry=DEFAULT_RETRY,
14591468
*,
14601469
soft_deleted=None,
1470+
return_partial_success=None,
14611471
):
14621472
"""Get all buckets in the project associated to the client.
14631473
@@ -1516,6 +1526,13 @@ def list_buckets(
15161526
generation number. This parameter can only be used successfully if the bucket has a soft delete policy.
15171527
See: https://cloud.google.com/storage/docs/soft-delete
15181528
1529+
:type return_partial_success: bool
1530+
:param return_partial_success:
1531+
(Optional) If True, the response will also contain a list of
1532+
unreachable buckets if the buckets are unavailable. The
1533+
unreachable buckets will be available on the ``unreachable``
1534+
attribute of the returned iterator.
1535+
15191536
:rtype: :class:`~google.api_core.page_iterator.Iterator`
15201537
:raises ValueError: if both ``project`` is ``None`` and the client's
15211538
project is also ``None``.
@@ -1551,7 +1568,10 @@ def list_buckets(
15511568
if soft_deleted is not None:
15521569
extra_params["softDeleted"] = soft_deleted
15531570

1554-
return self._list_resource(
1571+
if return_partial_success is not None:
1572+
extra_params["returnPartialSuccess"] = return_partial_success
1573+
1574+
iterator = self._list_resource(
15551575
"/b",
15561576
_item_to_bucket,
15571577
page_token=page_token,
@@ -1560,7 +1580,9 @@ def list_buckets(
15601580
page_size=page_size,
15611581
timeout=timeout,
15621582
retry=retry,
1583+
page_start=_buckets_page_start,
15631584
)
1585+
return iterator
15641586

15651587
def restore_bucket(
15661588
self,

tests/unit/test_client.py

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2224,7 +2224,7 @@ def test_list_blobs_w_explicit_w_user_project(self):
22242224

22252225
def test_list_buckets_wo_project(self):
22262226
from google.cloud.exceptions import BadRequest
2227-
from google.cloud.storage.client import _item_to_bucket
2227+
from google.cloud.storage.client import _item_to_bucket, _buckets_page_start
22282228

22292229
credentials = _make_credentials()
22302230
client = self._make_one(project=None, credentials=credentials)
@@ -2253,10 +2253,11 @@ def test_list_buckets_wo_project(self):
22532253
page_size=expected_page_size,
22542254
timeout=self._get_default_timeout(),
22552255
retry=DEFAULT_RETRY,
2256+
page_start=_buckets_page_start,
22562257
)
22572258

22582259
def test_list_buckets_wo_project_w_emulator(self):
2259-
from google.cloud.storage.client import _item_to_bucket
2260+
from google.cloud.storage.client import _item_to_bucket, _buckets_page_start
22602261

22612262
# mock STORAGE_EMULATOR_ENV_VAR is set
22622263
host = "http://localhost:8080"
@@ -2288,10 +2289,11 @@ def test_list_buckets_wo_project_w_emulator(self):
22882289
page_size=expected_page_size,
22892290
timeout=self._get_default_timeout(),
22902291
retry=DEFAULT_RETRY,
2292+
page_start=_buckets_page_start,
22912293
)
22922294

22932295
def test_list_buckets_w_environ_project_w_emulator(self):
2294-
from google.cloud.storage.client import _item_to_bucket
2296+
from google.cloud.storage.client import _item_to_bucket, _buckets_page_start
22952297

22962298
# mock STORAGE_EMULATOR_ENV_VAR is set
22972299
host = "http://localhost:8080"
@@ -2327,10 +2329,11 @@ def test_list_buckets_w_environ_project_w_emulator(self):
23272329
page_size=expected_page_size,
23282330
timeout=self._get_default_timeout(),
23292331
retry=DEFAULT_RETRY,
2332+
page_start=_buckets_page_start,
23302333
)
23312334

23322335
def test_list_buckets_w_custom_endpoint(self):
2333-
from google.cloud.storage.client import _item_to_bucket
2336+
from google.cloud.storage.client import _item_to_bucket, _buckets_page_start
23342337

23352338
custom_endpoint = "storage-example.p.googleapis.com"
23362339
client = self._make_one(client_options={"api_endpoint": custom_endpoint})
@@ -2358,10 +2361,11 @@ def test_list_buckets_w_custom_endpoint(self):
23582361
page_size=expected_page_size,
23592362
timeout=self._get_default_timeout(),
23602363
retry=DEFAULT_RETRY,
2364+
page_start=_buckets_page_start,
23612365
)
23622366

23632367
def test_list_buckets_w_defaults(self):
2364-
from google.cloud.storage.client import _item_to_bucket
2368+
from google.cloud.storage.client import _item_to_bucket, _buckets_page_start
23652369

23662370
project = "PROJECT"
23672371
credentials = _make_credentials()
@@ -2390,10 +2394,11 @@ def test_list_buckets_w_defaults(self):
23902394
page_size=expected_page_size,
23912395
timeout=self._get_default_timeout(),
23922396
retry=DEFAULT_RETRY,
2397+
page_start=_buckets_page_start,
23932398
)
23942399

23952400
def test_list_buckets_w_soft_deleted(self):
2396-
from google.cloud.storage.client import _item_to_bucket
2401+
from google.cloud.storage.client import _item_to_bucket, _buckets_page_start
23972402

23982403
project = "PROJECT"
23992404
credentials = _make_credentials()
@@ -2423,10 +2428,11 @@ def test_list_buckets_w_soft_deleted(self):
24232428
page_size=expected_page_size,
24242429
timeout=self._get_default_timeout(),
24252430
retry=DEFAULT_RETRY,
2431+
page_start=_buckets_page_start,
24262432
)
24272433

24282434
def test_list_buckets_w_explicit(self):
2429-
from google.cloud.storage.client import _item_to_bucket
2435+
from google.cloud.storage.client import _item_to_bucket, _buckets_page_start
24302436

24312437
project = "foo-bar"
24322438
other_project = "OTHER_PROJECT"
@@ -2476,6 +2482,7 @@ def test_list_buckets_w_explicit(self):
24762482
page_size=expected_page_size,
24772483
timeout=timeout,
24782484
retry=retry,
2485+
page_start=_buckets_page_start,
24792486
)
24802487

24812488
def test_restore_bucket(self):
@@ -3086,6 +3093,57 @@ def test_get_signed_policy_v4_with_access_token_sa_email(self):
30863093
self.assertEqual(fields["x-goog-signature"], EXPECTED_SIGN)
30873094
self.assertEqual(fields["policy"], EXPECTED_POLICY)
30883095

3096+
def test_list_buckets_w_partial_success(self):
3097+
from google.cloud.storage.client import _item_to_bucket
3098+
from google.cloud.storage.client import _buckets_page_start
3099+
3100+
PROJECT = "project"
3101+
bucket_name = "bucket-name"
3102+
unreachable_bucket = "projects/_/buckets/unreachable-bucket"
3103+
3104+
client = self._make_one(project=PROJECT)
3105+
3106+
mock_bucket = mock.Mock()
3107+
mock_bucket.name = bucket_name
3108+
3109+
mock_page = mock.Mock()
3110+
mock_page.unreachable = [unreachable_bucket]
3111+
mock_page.__iter__ = mock.Mock(return_value=iter([mock_bucket]))
3112+
3113+
mock_iterator = mock.Mock()
3114+
mock_iterator.pages = iter([mock_page])
3115+
3116+
client._list_resource = mock.Mock(return_value=mock_iterator)
3117+
3118+
iterator = client.list_buckets(return_partial_success=True)
3119+
3120+
page = next(iterator.pages)
3121+
3122+
self.assertEqual(page.unreachable, [unreachable_bucket])
3123+
3124+
buckets = list(page)
3125+
self.assertEqual(len(buckets), 1)
3126+
self.assertEqual(buckets[0].name, bucket_name)
3127+
3128+
expected_path = "/b"
3129+
expected_item_to_value = _item_to_bucket
3130+
expected_extra_params = {
3131+
"project": PROJECT,
3132+
"projection": "noAcl",
3133+
"returnPartialSuccess": True,
3134+
}
3135+
3136+
client._list_resource.assert_called_once_with(
3137+
expected_path,
3138+
expected_item_to_value,
3139+
page_token=None,
3140+
max_results=None,
3141+
extra_params=expected_extra_params,
3142+
page_size=None,
3143+
timeout=self._get_default_timeout(),
3144+
retry=DEFAULT_RETRY,
3145+
page_start=_buckets_page_start,
3146+
)
30893147

30903148
class Test__item_to_bucket(unittest.TestCase):
30913149
def _call_fut(self, iterator, item):

0 commit comments

Comments
 (0)