From 59b7b26b9f0e7d0da7b8b243e82c29c305b79019 Mon Sep 17 00:00:00 2001 From: Chandra Date: Mon, 26 Jan 2026 11:34:22 +0000 Subject: [PATCH 1/2] init code for exposing metadata ops into grpcCient --- .../asyncio/async_grpc_client.py | 81 +++++++++++++++++++ tests/system/test_zonal.py | 32 +++++++- tests/unit/asyncio/test_async_grpc_client.py | 43 ++++++++++ 3 files changed, 155 insertions(+), 1 deletion(-) diff --git a/google/cloud/storage/_experimental/asyncio/async_grpc_client.py b/google/cloud/storage/_experimental/asyncio/async_grpc_client.py index b455b1c29..e1ba3b858 100644 --- a/google/cloud/storage/_experimental/asyncio/async_grpc_client.py +++ b/google/cloud/storage/_experimental/asyncio/async_grpc_client.py @@ -42,6 +42,7 @@ class AsyncGrpcClient: (Optional) Whether to attempt to use DirectPath for gRPC connections. Defaults to ``True``. """ + def __init__( self, credentials=None, @@ -97,3 +98,83 @@ 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: long + :param if_generation_match: + (Optional) See :ref:`using-if-generation-match` + + :type if_generation_not_match: long + :param if_generation_not_match: + (Optional) See :ref:`using-if-generation-not-match` + + :type if_metageneration_match: long + :param if_metageneration_match: + (Optional) See :ref:`using-if-metageneration-match` + + :type if_metageneration_not_match: long + :param if_metageneration_not_match: + (Optional) See :ref:`using-if-metageneration-not-match` + + + """ + # 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) + + +if __name__ == "__main__": + import asyncio + + # This is a sample showing how to use the delete_object method. + # To run this sample, install the library and set up authentication. + # See google-cloud-storage documentation for more details. + # + # You will need to replace `your-bucket-name` and `your-object-name` + # with your actual bucket name and object name. + + BUCKET_NAME = "chandrasiri-benchmarks-zb" + OBJECT_NAME = "demo-1GiB-100-objects-298" + + async def main(): + client = AsyncGrpcClient() + print(f"Attempting to delete {OBJECT_NAME} from bucket {BUCKET_NAME}...") + try: + await client.delete_object(BUCKET_NAME, OBJECT_NAME, if_generation_match=1768326969788933) + print("Object deleted successfully.") + except Exception as e: + print(f"An error occurred: {e}") + raise + + asyncio.run(main()) diff --git a/tests/system/test_zonal.py b/tests/system/test_zonal.py index 0bd61ff53..0545d5d49 100644 --- a/tests/system/test_zonal.py +++ b/tests/system/test_zonal.py @@ -19,7 +19,7 @@ from google.cloud.storage._experimental.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,33 @@ 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 +): + """ + 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, _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.delete_object(bucket=f"projects/_/buckets/{_ZONAL_BUCKET}", object_=object_name) + + + # trying to get raises raises 404. + with pytest.raises(NotFound): + await 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 02b416f6b..1c4c7271b 100644 --- a/tests/unit/asyncio/test_async_grpc_client.py +++ b/tests/unit/asyncio/test_async_grpc_client.py @@ -182,3 +182,46 @@ def test_grpc_client_with_anon_creds(self, mock_grpc_gapic_client): options=expected_options, ) mock_transport_cls.assert_called_once_with(channel=channel_sentinel) + + +# TODO(developer): Add unit tests for all the methods in async_grpc_client.py +class TestDeleteObject(unittest.IsolatedAsyncioTestCase): + @mock.patch("google.cloud._storage_v2.StorageAsyncClient") + async def test_delete_object(self, mock_async_storage_client): + # Arrange + client = async_grpc_client.AsyncGrpcClient( + credentials=_make_credentials(spec=AnonymousCredentials) + ) + client._grpc_client = mock.AsyncMock() + + 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 = client._grpc_client.delete_object.call_args + request = call_kwargs["request"] + self.assertEqual(request.bucket, "projects/_/buckets/bucket") + self.assertEqual(request.object, "object") + self.assertEqual(request.generation, generation) + self.assertEqual(request.if_generation_match, if_generation_match) + self.assertEqual(request.if_generation_not_match, if_generation_not_match) + self.assertEqual(request.if_metageneration_match, if_metageneration_match) + self.assertEqual( + request.if_metageneration_not_match, if_metageneration_not_match + ) From c0cdd2607bff714d976f7fbe44d33d16b3a34911 Mon Sep 17 00:00:00 2001 From: Chandra Date: Thu, 29 Jan 2026 13:10:43 +0000 Subject: [PATCH 2/2] add lint & formatting changes --- .../storage/asyncio/async_grpc_client.py | 33 +++---------------- tests/system/test_zonal.py | 17 +++++----- tests/unit/asyncio/test_async_grpc_client.py | 2 -- 3 files changed, 13 insertions(+), 39 deletions(-) diff --git a/google/cloud/storage/asyncio/async_grpc_client.py b/google/cloud/storage/asyncio/async_grpc_client.py index 27eaf00f2..3ff846b5f 100644 --- a/google/cloud/storage/asyncio/async_grpc_client.py +++ b/google/cloud/storage/asyncio/async_grpc_client.py @@ -130,7 +130,7 @@ async def delete_object( :param generation: (Optional) If present, permanently deletes a specific generation of an object. - + :type if_generation_match: int :param if_generation_match: (Optional) @@ -148,7 +148,9 @@ async def delete_object( # 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, + 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, @@ -156,30 +158,3 @@ async def delete_object( **kwargs, ) await self._grpc_client.delete_object(request=request) - - -if __name__ == "__main__": - import asyncio - - # This is a sample showing how to use the delete_object method. - # To run this sample, install the library and set up authentication. - # See google-cloud-storage documentation for more details. - # - # You will need to replace `your-bucket-name` and `your-object-name` - # with your actual bucket name and object name. - - BUCKET_NAME = "chandrasiri-benchmarks-zb" - OBJECT_NAME = "test_md_1" - - async def main(): - client = AsyncGrpcClient() - print(f"Attempting to delete {OBJECT_NAME} from bucket {BUCKET_NAME}...") - try: - await client.delete_object(BUCKET_NAME, OBJECT_NAME) - # if_generation_match=1768326969788933) - print("Object deleted successfully.") - except Exception as e: - print(f"An error occurred: {e}") - raise - - asyncio.run(main()) \ No newline at end of file diff --git a/tests/system/test_zonal.py b/tests/system/test_zonal.py index b418097d1..9bd870860 100644 --- a/tests/system/test_zonal.py +++ b/tests/system/test_zonal.py @@ -571,9 +571,8 @@ async def _run(): event_loop.run_until_complete(_run()) -def test_delete_object_using_grpc_client( - event_loop, grpc_client -): + +def test_delete_object_using_grpc_client(event_loop, grpc_client_direct): """ Test that a new writer when specifies `None` overrides the existing object. """ @@ -581,20 +580,22 @@ def test_delete_object_using_grpc_client( async def _run(): writer = AsyncAppendableObjectWriter( - grpc_client, _ZONAL_BUCKET, object_name, generation=0 + grpc_client_direct, _ZONAL_BUCKET, object_name, generation=0 ) # Empty object is created. await writer.open() - await writer.append(b'some_bytes') + await writer.append(b"some_bytes") await writer.close() - await grpc_client.delete_object(bucket=f"projects/_/buckets/{_ZONAL_BUCKET}", object_=object_name) - + await grpc_client_direct.delete_object(_ZONAL_BUCKET, object_name) # trying to get raises raises 404. with pytest.raises(NotFound): - await grpc_client.get_object(bucket=f"projects/_/buckets/{_ZONAL_BUCKET}", object_=object_name) + # 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() diff --git a/tests/unit/asyncio/test_async_grpc_client.py b/tests/unit/asyncio/test_async_grpc_client.py index 8524606cf..e7d649e72 100644 --- a/tests/unit/asyncio/test_async_grpc_client.py +++ b/tests/unit/asyncio/test_async_grpc_client.py @@ -188,7 +188,6 @@ def test_grpc_client_with_anon_creds(self, mock_grpc_gapic_client): ) 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): """Test that gcloud-python user agent is appended to existing user agent. @@ -255,4 +254,3 @@ async def test_delete_object(self, mock_async_storage_client): 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 -