Skip to content

Commit 0e5ec29

Browse files
authored
feat: add get_object method for async grpc client (#1735)
This method can be used to fetch the metadata of an object using the async grpc API.
1 parent 7a00dfb commit 0e5ec29

File tree

3 files changed

+191
-0
lines changed

3 files changed

+191
-0
lines changed

google/cloud/storage/asyncio/async_grpc_client.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,66 @@ async def delete_object(
158158
**kwargs,
159159
)
160160
await self._grpc_client.delete_object(request=request)
161+
162+
async def get_object(
163+
self,
164+
bucket_name,
165+
object_name,
166+
generation=None,
167+
if_generation_match=None,
168+
if_generation_not_match=None,
169+
if_metageneration_match=None,
170+
if_metageneration_not_match=None,
171+
soft_deleted=None,
172+
**kwargs,
173+
):
174+
"""Retrieves an object's metadata.
175+
176+
In the gRPC API, this is performed by the GetObject RPC, which
177+
returns the object resource (metadata) without the object's data.
178+
179+
:type bucket_name: str
180+
:param bucket_name: The name of the bucket in which the object resides.
181+
182+
:type object_name: str
183+
:param object_name: The name of the object.
184+
185+
:type generation: int
186+
:param generation:
187+
(Optional) If present, selects a specific generation of an object.
188+
189+
:type if_generation_match: int
190+
:param if_generation_match: (Optional) Precondition for object generation match.
191+
192+
:type if_generation_not_match: int
193+
:param if_generation_not_match: (Optional) Precondition for object generation mismatch.
194+
195+
:type if_metageneration_match: int
196+
:param if_metageneration_match: (Optional) Precondition for metageneration match.
197+
198+
:type if_metageneration_not_match: int
199+
:param if_metageneration_not_match: (Optional) Precondition for metageneration mismatch.
200+
201+
:type soft_deleted: bool
202+
:param soft_deleted:
203+
(Optional) If True, return the soft-deleted version of this object.
204+
205+
:rtype: :class:`google.cloud._storage_v2.types.Object`
206+
:returns: The object metadata resource.
207+
"""
208+
bucket_path = f"projects/_/buckets/{bucket_name}"
209+
210+
request = storage_v2.GetObjectRequest(
211+
bucket=bucket_path,
212+
object=object_name,
213+
generation=generation,
214+
if_generation_match=if_generation_match,
215+
if_generation_not_match=if_generation_not_match,
216+
if_metageneration_match=if_metageneration_match,
217+
if_metageneration_not_match=if_metageneration_not_match,
218+
soft_deleted=soft_deleted or False,
219+
**kwargs,
220+
)
221+
222+
# Calls the underlying GAPIC StorageAsyncClient.get_object method
223+
return await self._grpc_client.get_object(request=request)

tests/system/test_zonal.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,3 +601,56 @@ async def _run():
601601
gc.collect()
602602

603603
event_loop.run_until_complete(_run())
604+
605+
def test_get_object_after_appendable_write(
606+
grpc_clients,
607+
grpc_client_direct,
608+
event_loop,
609+
storage_client,
610+
blobs_to_delete,
611+
):
612+
"""Test getting object metadata after writing with AsyncAppendableObjectWriter.
613+
614+
This test:
615+
1. Creates a test object using AsyncAppendableObjectWriter
616+
2. Appends content to the object (without finalizing)
617+
3. Closes the write stream
618+
4. Fetches the object metadata using AsyncGrpcClient.get_object()
619+
5. Verifies the object size matches the written data
620+
"""
621+
622+
async def _run():
623+
grpc_client = grpc_client_direct
624+
object_name = f"test-get-object-{uuid.uuid4().hex}"
625+
test_data = b"Some test data bytes."
626+
expected_size = len(test_data)
627+
628+
writer = AsyncAppendableObjectWriter(
629+
grpc_client,
630+
_ZONAL_BUCKET,
631+
object_name,
632+
)
633+
634+
await writer.open()
635+
await writer.append(test_data)
636+
await writer.close(finalize_on_close=False)
637+
638+
obj = await grpc_client.get_object(
639+
bucket_name=_ZONAL_BUCKET,
640+
object_name=object_name,
641+
)
642+
643+
# Assert
644+
assert obj is not None
645+
assert obj.name == object_name
646+
assert obj.bucket == f"projects/_/buckets/{_ZONAL_BUCKET}"
647+
assert obj.size == expected_size, (
648+
f"Expected object size {expected_size}, got {obj.size}"
649+
)
650+
651+
# Cleanup
652+
blobs_to_delete.append(storage_client.bucket(_ZONAL_BUCKET).blob(object_name))
653+
del writer
654+
gc.collect()
655+
656+
event_loop.run_until_complete(_run())

tests/unit/asyncio/test_async_grpc_client.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,3 +254,78 @@ async def test_delete_object(self, mock_async_storage_client):
254254
assert request.if_generation_not_match == if_generation_not_match
255255
assert request.if_metageneration_match == if_metageneration_match
256256
assert request.if_metageneration_not_match == if_metageneration_not_match
257+
258+
@mock.patch("google.cloud._storage_v2.StorageAsyncClient")
259+
@pytest.mark.asyncio
260+
async def test_get_object(self, mock_async_storage_client):
261+
# Arrange
262+
mock_transport_cls = mock.MagicMock()
263+
mock_async_storage_client.get_transport_class.return_value = mock_transport_cls
264+
mock_gapic_client = mock.AsyncMock()
265+
mock_async_storage_client.return_value = mock_gapic_client
266+
267+
client = async_grpc_client.AsyncGrpcClient(
268+
credentials=_make_credentials(spec=AnonymousCredentials)
269+
)
270+
271+
bucket_name = "bucket"
272+
object_name = "object"
273+
274+
# Act
275+
await client.get_object(
276+
bucket_name,
277+
object_name,
278+
)
279+
280+
# Assert
281+
call_args, call_kwargs = mock_gapic_client.get_object.call_args
282+
request = call_kwargs["request"]
283+
assert request.bucket == "projects/_/buckets/bucket"
284+
assert request.object == "object"
285+
assert request.soft_deleted is False
286+
287+
@mock.patch("google.cloud._storage_v2.StorageAsyncClient")
288+
@pytest.mark.asyncio
289+
async def test_get_object_with_all_parameters(self, mock_async_storage_client):
290+
# Arrange
291+
mock_transport_cls = mock.MagicMock()
292+
mock_async_storage_client.get_transport_class.return_value = mock_transport_cls
293+
mock_gapic_client = mock.AsyncMock()
294+
mock_async_storage_client.return_value = mock_gapic_client
295+
296+
client = async_grpc_client.AsyncGrpcClient(
297+
credentials=_make_credentials(spec=AnonymousCredentials)
298+
)
299+
300+
bucket_name = "bucket"
301+
object_name = "object"
302+
generation = 123
303+
if_generation_match = 456
304+
if_generation_not_match = 789
305+
if_metageneration_match = 111
306+
if_metageneration_not_match = 222
307+
soft_deleted = True
308+
309+
# Act
310+
await client.get_object(
311+
bucket_name,
312+
object_name,
313+
generation=generation,
314+
if_generation_match=if_generation_match,
315+
if_generation_not_match=if_generation_not_match,
316+
if_metageneration_match=if_metageneration_match,
317+
if_metageneration_not_match=if_metageneration_not_match,
318+
soft_deleted=soft_deleted,
319+
)
320+
321+
# Assert
322+
call_args, call_kwargs = mock_gapic_client.get_object.call_args
323+
request = call_kwargs["request"]
324+
assert request.bucket == "projects/_/buckets/bucket"
325+
assert request.object == "object"
326+
assert request.generation == generation
327+
assert request.if_generation_match == if_generation_match
328+
assert request.if_generation_not_match == if_generation_not_match
329+
assert request.if_metageneration_match == if_metageneration_match
330+
assert request.if_metageneration_not_match == if_metageneration_not_match
331+
assert request.soft_deleted is True

0 commit comments

Comments
 (0)