diff --git a/google/cloud/storage/asyncio/async_grpc_client.py b/google/cloud/storage/asyncio/async_grpc_client.py index 474b961b4..3ff846b5f 100644 --- a/google/cloud/storage/asyncio/async_grpc_client.py +++ b/google/cloud/storage/asyncio/async_grpc_client.py @@ -106,3 +106,55 @@ def grpc_client(self): google.cloud._storage_v2.StorageAsyncClient: The configured GAPIC client. """ return self._grpc_client + + async def delete_object( + self, + bucket_name, + object_name, + generation=None, + if_generation_match=None, + if_generation_not_match=None, + if_metageneration_match=None, + if_metageneration_not_match=None, + **kwargs, + ): + """Deletes an object and its metadata. + + :type bucket_name: str + :param bucket_name: The name of the bucket in which the object resides. + + :type object_name: str + :param object_name: The name of the object to delete. + + :type generation: int + :param generation: + (Optional) If present, permanently deletes a specific generation + of an object. + + :type if_generation_match: int + :param if_generation_match: (Optional) + + :type if_generation_not_match: int + :param if_generation_not_match: (Optional) + + :type if_metageneration_match: int + :param if_metageneration_match: (Optional) + + :type if_metageneration_not_match: int + :param if_metageneration_not_match: (Optional) + + + """ + # The gRPC API requires the bucket name to be in the format "projects/_/buckets/bucket_name" + bucket_path = f"projects/_/buckets/{bucket_name}" + request = storage_v2.DeleteObjectRequest( + bucket=bucket_path, + object=object_name, + generation=generation, + if_generation_match=if_generation_match, + if_generation_not_match=if_generation_not_match, + if_metageneration_match=if_metageneration_match, + if_metageneration_not_match=if_metageneration_not_match, + **kwargs, + ) + await self._grpc_client.delete_object(request=request) diff --git a/tests/system/test_zonal.py b/tests/system/test_zonal.py index 42164d364..9bd870860 100644 --- a/tests/system/test_zonal.py +++ b/tests/system/test_zonal.py @@ -19,7 +19,7 @@ from google.cloud.storage.asyncio.async_multi_range_downloader import ( AsyncMultiRangeDownloader, ) -from google.api_core.exceptions import FailedPrecondition +from google.api_core.exceptions import FailedPrecondition, NotFound pytestmark = pytest.mark.skipif( @@ -570,3 +570,34 @@ async def _run(): blobs_to_delete.append(storage_client.bucket(_ZONAL_BUCKET).blob(object_name)) event_loop.run_until_complete(_run()) + + +def test_delete_object_using_grpc_client(event_loop, grpc_client_direct): + """ + Test that a new writer when specifies `None` overrides the existing object. + """ + object_name = f"test_append_with_generation-{uuid.uuid4()}" + + async def _run(): + writer = AsyncAppendableObjectWriter( + grpc_client_direct, _ZONAL_BUCKET, object_name, generation=0 + ) + + # Empty object is created. + await writer.open() + await writer.append(b"some_bytes") + await writer.close() + + await grpc_client_direct.delete_object(_ZONAL_BUCKET, object_name) + + # trying to get raises raises 404. + with pytest.raises(NotFound): + # TODO: Remove this once GET_OBJECT is exposed in `AsyncGrpcClient` + await grpc_client_direct._grpc_client.get_object( + bucket=f"projects/_/buckets/{_ZONAL_BUCKET}", object_=object_name + ) + # cleanup + del writer + gc.collect() + + event_loop.run_until_complete(_run()) diff --git a/tests/unit/asyncio/test_async_grpc_client.py b/tests/unit/asyncio/test_async_grpc_client.py index 3d897d336..e7d649e72 100644 --- a/tests/unit/asyncio/test_async_grpc_client.py +++ b/tests/unit/asyncio/test_async_grpc_client.py @@ -13,6 +13,7 @@ # limitations under the License. from unittest import mock +import pytest from google.auth import credentials as auth_credentials from google.auth.credentials import AnonymousCredentials from google.api_core import client_info as client_info_lib @@ -185,6 +186,7 @@ def test_grpc_client_with_anon_creds(self, mock_grpc_gapic_client): credentials=anonymous_creds, options=expected_options, ) + mock_transport_cls.assert_called_once_with(channel=channel_sentinel) @mock.patch("google.cloud._storage_v2.StorageAsyncClient") def test_user_agent_with_custom_client_info(self, mock_async_storage_client): @@ -209,3 +211,46 @@ def test_user_agent_with_custom_client_info(self, mock_async_storage_client): agent_version = f"gcloud-python/{__version__}" expected_user_agent = f"custom-app/1.0 {agent_version} " assert client_info.user_agent == expected_user_agent + + @mock.patch("google.cloud._storage_v2.StorageAsyncClient") + @pytest.mark.asyncio + async def test_delete_object(self, mock_async_storage_client): + # Arrange + mock_transport_cls = mock.MagicMock() + mock_async_storage_client.get_transport_class.return_value = mock_transport_cls + mock_gapic_client = mock.AsyncMock() + mock_async_storage_client.return_value = mock_gapic_client + + client = async_grpc_client.AsyncGrpcClient( + credentials=_make_credentials(spec=AnonymousCredentials) + ) + + bucket_name = "bucket" + object_name = "object" + generation = 123 + if_generation_match = 456 + if_generation_not_match = 789 + if_metageneration_match = 111 + if_metageneration_not_match = 222 + + # Act + await client.delete_object( + bucket_name, + object_name, + generation=generation, + if_generation_match=if_generation_match, + if_generation_not_match=if_generation_not_match, + if_metageneration_match=if_metageneration_match, + if_metageneration_not_match=if_metageneration_not_match, + ) + + # Assert + call_args, call_kwargs = mock_gapic_client.delete_object.call_args + request = call_kwargs["request"] + assert request.bucket == "projects/_/buckets/bucket" + assert request.object == "object" + assert request.generation == generation + assert request.if_generation_match == if_generation_match + assert request.if_generation_not_match == if_generation_not_match + assert request.if_metageneration_match == if_metageneration_match + assert request.if_metageneration_not_match == if_metageneration_not_match