Skip to content

Commit c8dd7a0

Browse files
authored
feat: expose DELETE_OBJECT in AsyncGrpcClient (#1718)
Expose `DELETE_OBJECT` in `AsyncGrpcClient`
1 parent 08bc708 commit c8dd7a0

File tree

3 files changed

+129
-1
lines changed

3 files changed

+129
-1
lines changed

google/cloud/storage/asyncio/async_grpc_client.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,55 @@ def grpc_client(self):
106106
google.cloud._storage_v2.StorageAsyncClient: The configured GAPIC client.
107107
"""
108108
return self._grpc_client
109+
110+
async def delete_object(
111+
self,
112+
bucket_name,
113+
object_name,
114+
generation=None,
115+
if_generation_match=None,
116+
if_generation_not_match=None,
117+
if_metageneration_match=None,
118+
if_metageneration_not_match=None,
119+
**kwargs,
120+
):
121+
"""Deletes an object and its metadata.
122+
123+
:type bucket_name: str
124+
:param bucket_name: The name of the bucket in which the object resides.
125+
126+
:type object_name: str
127+
:param object_name: The name of the object to delete.
128+
129+
:type generation: int
130+
:param generation:
131+
(Optional) If present, permanently deletes a specific generation
132+
of an object.
133+
134+
:type if_generation_match: int
135+
:param if_generation_match: (Optional)
136+
137+
:type if_generation_not_match: int
138+
:param if_generation_not_match: (Optional)
139+
140+
:type if_metageneration_match: int
141+
:param if_metageneration_match: (Optional)
142+
143+
:type if_metageneration_not_match: int
144+
:param if_metageneration_not_match: (Optional)
145+
146+
147+
"""
148+
# The gRPC API requires the bucket name to be in the format "projects/_/buckets/bucket_name"
149+
bucket_path = f"projects/_/buckets/{bucket_name}"
150+
request = storage_v2.DeleteObjectRequest(
151+
bucket=bucket_path,
152+
object=object_name,
153+
generation=generation,
154+
if_generation_match=if_generation_match,
155+
if_generation_not_match=if_generation_not_match,
156+
if_metageneration_match=if_metageneration_match,
157+
if_metageneration_not_match=if_metageneration_not_match,
158+
**kwargs,
159+
)
160+
await self._grpc_client.delete_object(request=request)

tests/system/test_zonal.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from google.cloud.storage.asyncio.async_multi_range_downloader import (
2020
AsyncMultiRangeDownloader,
2121
)
22-
from google.api_core.exceptions import FailedPrecondition
22+
from google.api_core.exceptions import FailedPrecondition, NotFound
2323

2424

2525
pytestmark = pytest.mark.skipif(
@@ -570,3 +570,34 @@ async def _run():
570570
blobs_to_delete.append(storage_client.bucket(_ZONAL_BUCKET).blob(object_name))
571571

572572
event_loop.run_until_complete(_run())
573+
574+
575+
def test_delete_object_using_grpc_client(event_loop, grpc_client_direct):
576+
"""
577+
Test that a new writer when specifies `None` overrides the existing object.
578+
"""
579+
object_name = f"test_append_with_generation-{uuid.uuid4()}"
580+
581+
async def _run():
582+
writer = AsyncAppendableObjectWriter(
583+
grpc_client_direct, _ZONAL_BUCKET, object_name, generation=0
584+
)
585+
586+
# Empty object is created.
587+
await writer.open()
588+
await writer.append(b"some_bytes")
589+
await writer.close()
590+
591+
await grpc_client_direct.delete_object(_ZONAL_BUCKET, object_name)
592+
593+
# trying to get raises raises 404.
594+
with pytest.raises(NotFound):
595+
# TODO: Remove this once GET_OBJECT is exposed in `AsyncGrpcClient`
596+
await grpc_client_direct._grpc_client.get_object(
597+
bucket=f"projects/_/buckets/{_ZONAL_BUCKET}", object_=object_name
598+
)
599+
# cleanup
600+
del writer
601+
gc.collect()
602+
603+
event_loop.run_until_complete(_run())

tests/unit/asyncio/test_async_grpc_client.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
from unittest import mock
16+
import pytest
1617
from google.auth import credentials as auth_credentials
1718
from google.auth.credentials import AnonymousCredentials
1819
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):
185186
credentials=anonymous_creds,
186187
options=expected_options,
187188
)
189+
mock_transport_cls.assert_called_once_with(channel=channel_sentinel)
188190

189191
@mock.patch("google.cloud._storage_v2.StorageAsyncClient")
190192
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):
209211
agent_version = f"gcloud-python/{__version__}"
210212
expected_user_agent = f"custom-app/1.0 {agent_version} "
211213
assert client_info.user_agent == expected_user_agent
214+
215+
@mock.patch("google.cloud._storage_v2.StorageAsyncClient")
216+
@pytest.mark.asyncio
217+
async def test_delete_object(self, mock_async_storage_client):
218+
# Arrange
219+
mock_transport_cls = mock.MagicMock()
220+
mock_async_storage_client.get_transport_class.return_value = mock_transport_cls
221+
mock_gapic_client = mock.AsyncMock()
222+
mock_async_storage_client.return_value = mock_gapic_client
223+
224+
client = async_grpc_client.AsyncGrpcClient(
225+
credentials=_make_credentials(spec=AnonymousCredentials)
226+
)
227+
228+
bucket_name = "bucket"
229+
object_name = "object"
230+
generation = 123
231+
if_generation_match = 456
232+
if_generation_not_match = 789
233+
if_metageneration_match = 111
234+
if_metageneration_not_match = 222
235+
236+
# Act
237+
await client.delete_object(
238+
bucket_name,
239+
object_name,
240+
generation=generation,
241+
if_generation_match=if_generation_match,
242+
if_generation_not_match=if_generation_not_match,
243+
if_metageneration_match=if_metageneration_match,
244+
if_metageneration_not_match=if_metageneration_not_match,
245+
)
246+
247+
# Assert
248+
call_args, call_kwargs = mock_gapic_client.delete_object.call_args
249+
request = call_kwargs["request"]
250+
assert request.bucket == "projects/_/buckets/bucket"
251+
assert request.object == "object"
252+
assert request.generation == generation
253+
assert request.if_generation_match == if_generation_match
254+
assert request.if_generation_not_match == if_generation_not_match
255+
assert request.if_metageneration_match == if_metageneration_match
256+
assert request.if_metageneration_not_match == if_metageneration_not_match

0 commit comments

Comments
 (0)