diff --git a/google/cloud/storage/bucket.py b/google/cloud/storage/bucket.py index 0f615f843..c3a1a0523 100644 --- a/google/cloud/storage/bucket.py +++ b/google/cloud/storage/bucket.py @@ -1290,6 +1290,7 @@ def list_blobs( client=None, timeout=_DEFAULT_TIMEOUT, retry=DEFAULT_RETRY, + match_glob=None, ): """Return an iterator used to find blobs in the bucket. @@ -1365,6 +1366,12 @@ def list_blobs( :param retry: (Optional) How to retry the RPC. See: :ref:`configuring_retries` + :type match_glob: str + :param match_glob: + (Optional) A glob pattern used to filter results (for example, foo*bar). + The string value must be UTF-8 encoded. See: + https://cloud.google.com/storage/docs/json_api/v1/objects/list#list-object-glob + :rtype: :class:`~google.api_core.page_iterator.Iterator` :returns: Iterator of all :class:`~google.cloud.storage.blob.Blob` in this bucket matching the arguments. @@ -1384,6 +1391,7 @@ def list_blobs( fields=fields, timeout=timeout, retry=retry, + match_glob=match_glob, ) def list_notifications( diff --git a/google/cloud/storage/client.py b/google/cloud/storage/client.py index 042e8b2ef..7df60c306 100644 --- a/google/cloud/storage/client.py +++ b/google/cloud/storage/client.py @@ -1127,6 +1127,7 @@ def list_blobs( page_size=None, timeout=_DEFAULT_TIMEOUT, retry=DEFAULT_RETRY, + match_glob=None, ): """Return an iterator used to find blobs in the bucket. @@ -1220,6 +1221,11 @@ def list_blobs( See the retry.py source code and docstrings in this package (google.cloud.storage.retry) for information on retry types and how to configure them. + match_glob (str): + (Optional) A glob pattern used to filter results (for example, foo*bar). + The string value must be UTF-8 encoded. See: + https://cloud.google.com/storage/docs/json_api/v1/objects/list#list-object-glob + Returns: Iterator of all :class:`~google.cloud.storage.blob.Blob` in this bucket matching the arguments. The RPC call @@ -1238,6 +1244,9 @@ def list_blobs( if delimiter is not None: extra_params["delimiter"] = delimiter + if match_glob is not None: + extra_params["matchGlob"] = match_glob + if start_offset is not None: extra_params["startOffset"] = start_offset diff --git a/tests/system/test_bucket.py b/tests/system/test_bucket.py index 0fb3a2f8d..6a2698e29 100644 --- a/tests/system/test_bucket.py +++ b/tests/system/test_bucket.py @@ -621,6 +621,38 @@ def test_bucket_list_blobs_hierarchy_w_include_trailing_delimiter( assert iterator.prefixes == expected_prefixes +@_helpers.retry_failures +def test_bucket_list_blobs_w_match_glob( + storage_client, + buckets_to_delete, + blobs_to_delete, +): + bucket_name = _helpers.unique_name("w-matchglob") + bucket = _helpers.retry_429_503(storage_client.create_bucket)(bucket_name) + buckets_to_delete.append(bucket) + + payload = b"helloworld" + blob_names = ["foo/bar", "foo/baz", "foo/foobar", "foobar"] + for name in blob_names: + blob = bucket.blob(name) + blob.upload_from_string(payload) + blobs_to_delete.append(blob) + + match_glob_results = { + "foo*bar": ["foobar"], + "foo**bar": ["foo/bar", "foo/foobar", "foobar"], + "**/foobar": ["foo/foobar", "foobar"], + "*/ba[rz]": ["foo/bar", "foo/baz"], + "*/ba[!a-y]": ["foo/baz"], + "**/{foobar,baz}": ["foo/baz", "foo/foobar", "foobar"], + "foo/{foo*,*baz}": ["foo/baz", "foo/foobar"], + } + for match_glob, expected_names in match_glob_results.items(): + blob_iter = bucket.list_blobs(match_glob=match_glob) + blobs = list(blob_iter) + assert [blob.name for blob in blobs] == expected_names + + def test_bucket_w_retention_period( storage_client, buckets_to_delete, diff --git a/tests/unit/test_bucket.py b/tests/unit/test_bucket.py index 15f6356f7..0c0873ee4 100644 --- a/tests/unit/test_bucket.py +++ b/tests/unit/test_bucket.py @@ -1143,6 +1143,7 @@ def test_list_blobs_w_defaults(self): expected_max_results = None expected_prefix = None expected_delimiter = None + expected_match_glob = None expected_start_offset = None expected_end_offset = None expected_include_trailing_delimiter = None @@ -1163,6 +1164,7 @@ def test_list_blobs_w_defaults(self): fields=expected_fields, timeout=self._get_default_timeout(), retry=DEFAULT_RETRY, + match_glob=expected_match_glob, ) def test_list_blobs_w_explicit(self): @@ -1171,6 +1173,7 @@ def test_list_blobs_w_explicit(self): page_token = "ABCD" prefix = "subfolder" delimiter = "/" + match_glob = "**txt" start_offset = "c" end_offset = "g" include_trailing_delimiter = True @@ -1197,6 +1200,7 @@ def test_list_blobs_w_explicit(self): client=other_client, timeout=timeout, retry=retry, + match_glob=match_glob, ) self.assertIs(iterator, other_client.list_blobs.return_value) @@ -1205,6 +1209,7 @@ def test_list_blobs_w_explicit(self): expected_max_results = max_results expected_prefix = prefix expected_delimiter = delimiter + expected_match_glob = match_glob expected_start_offset = start_offset expected_end_offset = end_offset expected_include_trailing_delimiter = include_trailing_delimiter @@ -1225,6 +1230,7 @@ def test_list_blobs_w_explicit(self): fields=expected_fields, timeout=timeout, retry=retry, + match_glob=expected_match_glob, ) def test_list_notifications_w_defaults(self): diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 0b5af95d6..31f7e3988 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -1928,6 +1928,7 @@ def test_list_blobs_w_explicit_w_user_project(self): page_token = "ABCD" prefix = "subfolder" delimiter = "/" + match_glob = "**txt" start_offset = "c" end_offset = "g" include_trailing_delimiter = True @@ -1962,6 +1963,7 @@ def test_list_blobs_w_explicit_w_user_project(self): page_size=page_size, timeout=timeout, retry=retry, + match_glob=match_glob, ) self.assertIs(iterator, client._list_resource.return_value) @@ -1976,6 +1978,7 @@ def test_list_blobs_w_explicit_w_user_project(self): "projection": projection, "prefix": prefix, "delimiter": delimiter, + "matchGlob": match_glob, "startOffset": start_offset, "endOffset": end_offset, "includeTrailingDelimiter": include_trailing_delimiter,