Skip to content
This repository was archived by the owner on Mar 31, 2026. It is now read-only.

Commit 51ec525

Browse files
authored
Merge branch 'main' into bench
2 parents d5a7efd + cc4831d commit 51ec525

21 files changed

Lines changed: 1295 additions & 44 deletions

cloudbuild/run_zonal_tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ export RUN_ZONAL_SYSTEM_TESTS=True
2626
CURRENT_ULIMIT=$(ulimit -n)
2727
echo '--- Running Zonal tests on VM with ulimit set to ---' $CURRENT_ULIMIT
2828
pytest -vv -s --log-format='%(asctime)s %(levelname)s %(message)s' --log-date-format='%H:%M:%S' tests/system/test_zonal.py
29+
pytest -vv -s --log-format='%(asctime)s %(levelname)s %(message)s' --log-date-format='%H:%M:%S' samples/snippets/zonal_buckets/zonal_snippets_test.py

google/cloud/storage/_experimental/asyncio/async_appendable_object_writer.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,29 @@ def __init__(
9797
:param object_name: The name of the GCS Appendable Object to be written.
9898
9999
:type generation: int
100-
:param generation: (Optional) If present, selects a specific revision of
101-
that object.
102-
If None, a new object is created.
103-
If None and Object already exists then it'll will be
104-
overwritten.
100+
:param generation: (Optional) If present, creates writer for that
101+
specific revision of that object. Use this to append data to an
102+
existing Appendable Object.
103+
104+
Setting to ``0`` makes the `writer.open()` succeed only if
105+
object doesn't exist in the bucket (useful for not accidentally
106+
overwriting existing objects).
107+
108+
Warning: If `None`, a new object is created. If an object with the
109+
same name already exists, it will be overwritten the moment
110+
`writer.open()` is called.
105111
106112
:type write_handle: bytes
107-
:param write_handle: (Optional) An existing handle for writing the object.
108-
If provided, opening the bidi-gRPC connection will be faster.
113+
:param write_handle: (Optional) An handle for writing the object.
114+
If provided, opening the bidi-gRPC connection will be faster.
115+
116+
:type writer_options: dict
117+
:param writer_options: (Optional) A dictionary of writer options.
118+
Supported options:
119+
- "FLUSH_INTERVAL_BYTES": int
120+
The number of bytes to append before "persisting" data in GCS
121+
servers. Default is `_DEFAULT_FLUSH_INTERVAL_BYTES`.
122+
Must be a multiple of `_MAX_CHUNK_SIZE_BYTES`.
109123
"""
110124
raise_if_no_fast_crc32c()
111125
self.client = client
@@ -133,7 +147,6 @@ def __init__(
133147
self.flush_interval = writer_options.get(
134148
"FLUSH_INTERVAL_BYTES", _DEFAULT_FLUSH_INTERVAL_BYTES
135149
)
136-
# TODO: add test case for this.
137150
if self.flush_interval < _MAX_CHUNK_SIZE_BYTES:
138151
raise exceptions.OutOfRange(
139152
f"flush_interval must be >= {_MAX_CHUNK_SIZE_BYTES} , but provided {self.flush_interval}"
@@ -346,6 +359,11 @@ async def finalize(self) -> _storage_v2.Object:
346359
self.offset = None
347360
return self.object_resource
348361

362+
@property
363+
def is_stream_open(self) -> bool:
364+
return self._is_stream_open
365+
366+
349367
# helper methods.
350368
async def append_from_string(self, data: str):
351369
"""

google/cloud/storage/_experimental/asyncio/async_grpc_client.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
"""An async client for interacting with Google Cloud Storage using the gRPC API."""
1616

1717
from google.cloud import _storage_v2 as storage_v2
18+
from google.cloud._storage_v2.services.storage.transports.base import (
19+
DEFAULT_CLIENT_INFO,
20+
)
1821

1922

2023
class AsyncGrpcClient:
@@ -30,7 +33,7 @@ class AsyncGrpcClient:
3033
The client info used to send a user-agent string along with API
3134
requests. If ``None``, then default info will be used.
3235
33-
:type client_options: :class:`~google.api_core.client_options.ClientOptions` or :class:`dict`
36+
:type client_options: :class:`~google.api_core.client_options.ClientOptions`
3437
:param client_options: (Optional) Client options used to set user options
3538
on the client.
3639
@@ -39,7 +42,6 @@ class AsyncGrpcClient:
3942
(Optional) Whether to attempt to use DirectPath for gRPC connections.
4043
Defaults to ``True``.
4144
"""
42-
4345
def __init__(
4446
self,
4547
credentials=None,
@@ -65,8 +67,15 @@ def _create_async_grpc_client(
6567
transport_cls = storage_v2.StorageAsyncClient.get_transport_class(
6668
"grpc_asyncio"
6769
)
70+
71+
if client_info is None:
72+
client_info = DEFAULT_CLIENT_INFO
73+
primary_user_agent = client_info.to_user_agent()
74+
6875
channel = transport_cls.create_channel(
69-
attempt_direct_path=attempt_direct_path, credentials=credentials
76+
attempt_direct_path=attempt_direct_path,
77+
credentials=credentials,
78+
options=(("grpc.primary_user_agent", primary_user_agent),),
7079
)
7180
transport = transport_cls(channel=channel)
7281

google/cloud/storage/_experimental/asyncio/async_multi_range_downloader.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,11 +303,19 @@ async def download_ranges(
303303
304304
:type read_ranges: List[Tuple[int, int, "BytesIO"]]
305305
:param read_ranges: A list of tuples, where each tuple represents a
306-
combintaion of byte_range and writeable buffer in format -
306+
combination of byte_range and writeable buffer in format -
307307
(`start_byte`, `bytes_to_read`, `writeable_buffer`). Buffer has
308308
to be provided by the user, and user has to make sure appropriate
309309
memory is available in the application to avoid out-of-memory crash.
310310
311+
Special cases:
312+
if the value of `bytes_to_read` is 0, it'll be interpreted as
313+
download all contents until the end of the file from `start_byte`.
314+
Examples:
315+
* (0, 0, buffer) : downloads 0 to end , i.e. entire object.
316+
* (100, 0, buffer) : downloads from 100 to end.
317+
318+
311319
:type lock: asyncio.Lock
312320
:param lock: (Optional) An asyncio lock to synchronize sends and recvs
313321
on the underlying bidi-GRPC stream. This is required when multiple

google/cloud/storage/_experimental/asyncio/async_read_object_stream.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,16 @@ async def close(self) -> None:
151151
"""Closes the bidi-gRPC connection."""
152152
if not self._is_stream_open:
153153
raise ValueError("Stream is not open")
154+
await self.requests_done()
154155
await self.socket_like_rpc.close()
155156
self._is_stream_open = False
156157

158+
async def requests_done(self):
159+
"""Signals that all requests have been sent."""
160+
161+
await self.socket_like_rpc.send(None)
162+
await self.socket_like_rpc.recv()
163+
157164
async def send(
158165
self, bidi_read_object_request: _storage_v2.BidiReadObjectRequest
159166
) -> None:

google/cloud/storage/_experimental/asyncio/async_write_object_stream.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,17 @@ class _AsyncWriteObjectStream(_AsyncAbstractObjectStream):
4747
:param object_name: The name of the GCS ``Appendable Object`` to be write.
4848
4949
:type generation_number: int
50-
:param generation_number: (Optional) If present, selects a specific revision of
51-
this object. If None, a new object is created.
50+
:param generation_number: (Optional) If present, creates writer for that
51+
specific revision of that object. Use this to append data to an
52+
existing Appendable Object.
53+
54+
Setting to ``0`` makes the `writer.open()` succeed only if
55+
object doesn't exist in the bucket (useful for not accidentally
56+
overwriting existing objects).
57+
58+
Warning: If `None`, a new object is created. If an object with the
59+
same name already exists, it will be overwritten the moment
60+
`writer.open()` is called.
5261
5362
:type write_handle: bytes
5463
:param write_handle: (Optional) An existing handle for writing the object.
@@ -92,22 +101,33 @@ def __init__(
92101
self.object_resource: Optional[_storage_v2.Object] = None
93102

94103
async def open(self) -> None:
95-
"""Opening an object for write , should do it's state lookup
96-
to know what's the persisted size is.
104+
"""
105+
Opens the bidi-gRPC connection to write to the object.
106+
107+
This method sends an initial request to start the stream and receives
108+
the first response containing metadata and a write handle.
109+
110+
:rtype: None
111+
:raises ValueError: If the stream is already open.
112+
:raises google.api_core.exceptions.FailedPrecondition:
113+
if `generation_number` is 0 and object already exists.
97114
"""
98115
if self._is_stream_open:
99116
raise ValueError("Stream is already open")
100117

101118
# Create a new object or overwrite existing one if generation_number
102119
# is None. This makes it consistent with GCS JSON API behavior.
103120
# Created object type would be Appendable Object.
104-
if self.generation_number is None:
121+
# if `generation_number` == 0 new object will be created only if there
122+
# isn't any existing object.
123+
if self.generation_number is None or self.generation_number == 0:
105124
self.first_bidi_write_req = _storage_v2.BidiWriteObjectRequest(
106125
write_object_spec=_storage_v2.WriteObjectSpec(
107126
resource=_storage_v2.Object(
108127
name=self.object_name, bucket=self._full_bucket_name
109128
),
110129
appendable=True,
130+
if_generation_match=self.generation_number,
111131
),
112132
)
113133
else:
@@ -118,7 +138,6 @@ async def open(self) -> None:
118138
generation=self.generation_number,
119139
),
120140
)
121-
122141
self.socket_like_rpc = AsyncBidiRpc(
123142
self.rpc, initial_request=self.first_bidi_write_req, metadata=self.metadata
124143
)
@@ -152,9 +171,16 @@ async def close(self) -> None:
152171
"""Closes the bidi-gRPC connection."""
153172
if not self._is_stream_open:
154173
raise ValueError("Stream is not open")
174+
await self.requests_done()
155175
await self.socket_like_rpc.close()
156176
self._is_stream_open = False
157177

178+
async def requests_done(self):
179+
"""Signals that all requests have been sent."""
180+
181+
await self.socket_like_rpc.send(None)
182+
await self.socket_like_rpc.recv()
183+
158184
async def send(
159185
self, bidi_write_object_request: _storage_v2.BidiWriteObjectRequest
160186
) -> None:
@@ -186,3 +212,4 @@ async def recv(self) -> _storage_v2.BidiWriteObjectResponse:
186212
@property
187213
def is_stream_open(self) -> bool:
188214
return self._is_stream_open
215+
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2026 Google Inc. All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the 'License');
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import argparse
18+
import asyncio
19+
20+
from google.cloud.storage._experimental.asyncio.async_appendable_object_writer import (
21+
AsyncAppendableObjectWriter,
22+
)
23+
from google.cloud.storage._experimental.asyncio.async_grpc_client import AsyncGrpcClient
24+
25+
26+
# [START storage_create_and_write_appendable_object]
27+
28+
29+
async def storage_create_and_write_appendable_object(
30+
bucket_name, object_name, grpc_client=None
31+
):
32+
"""Uploads an appendable object to zonal bucket.
33+
34+
grpc_client: an existing grpc_client to use, this is only for testing.
35+
"""
36+
37+
if grpc_client is None:
38+
grpc_client = AsyncGrpcClient().grpc_client
39+
writer = AsyncAppendableObjectWriter(
40+
client=grpc_client,
41+
bucket_name=bucket_name,
42+
object_name=object_name,
43+
generation=0, # throws `FailedPrecondition` if object already exists.
44+
)
45+
# This creates a new appendable object of size 0 and opens it for appending.
46+
await writer.open()
47+
48+
# appends data to the object
49+
# you can perform `.append` multiple times as needed. Data will be appended
50+
# to the end of the object.
51+
await writer.append(b"Some data")
52+
53+
# Once all appends are done, close the gRPC bidirectional stream.
54+
await writer.close()
55+
56+
print(
57+
f"Appended object {object_name} created of size {writer.persisted_size} bytes."
58+
)
59+
60+
61+
# [END storage_create_and_write_appendable_object]
62+
63+
if __name__ == "__main__":
64+
parser = argparse.ArgumentParser(
65+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
66+
)
67+
parser.add_argument("--bucket_name", help="Your Cloud Storage bucket name.")
68+
parser.add_argument("--object_name", help="Your Cloud Storage object name.")
69+
70+
args = parser.parse_args()
71+
72+
asyncio.run(
73+
storage_create_and_write_appendable_object(
74+
bucket_name=args.bucket_name,
75+
object_name=args.object_name,
76+
)
77+
)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2026 Google Inc. All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the 'License');
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import argparse
18+
import asyncio
19+
20+
from google.cloud.storage._experimental.asyncio.async_appendable_object_writer import (
21+
AsyncAppendableObjectWriter,
22+
)
23+
from google.cloud.storage._experimental.asyncio.async_grpc_client import AsyncGrpcClient
24+
25+
26+
# [START storage_finalize_appendable_object_upload]
27+
async def storage_finalize_appendable_object_upload(
28+
bucket_name, object_name, grpc_client=None
29+
):
30+
"""Creates, writes to, and finalizes an appendable object.
31+
32+
grpc_client: an existing grpc_client to use, this is only for testing.
33+
"""
34+
35+
if grpc_client is None:
36+
grpc_client = AsyncGrpcClient().grpc_client
37+
writer = AsyncAppendableObjectWriter(
38+
client=grpc_client,
39+
bucket_name=bucket_name,
40+
object_name=object_name,
41+
generation=0, # throws `FailedPrecondition` if object already exists.
42+
)
43+
# This creates a new appendable object of size 0 and opens it for appending.
44+
await writer.open()
45+
46+
# Appends data to the object.
47+
await writer.append(b"Some data")
48+
49+
# finalize the appendable object,
50+
# NOTE:
51+
# 1. once finalized no more appends can be done to the object.
52+
# 2. If you don't want to finalize, you can simply call `writer.close`
53+
# 3. calling `.finalize()` also closes the grpc-bidi stream, calling
54+
# `.close` after `.finalize` may lead to undefined behavior.
55+
object_resource = await writer.finalize()
56+
57+
print(f"Appendable object {object_name} created and finalized.")
58+
print("Object Metadata:")
59+
print(object_resource)
60+
61+
62+
# [END storage_finalize_appendable_object_upload]
63+
64+
if __name__ == "__main__":
65+
parser = argparse.ArgumentParser(
66+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
67+
)
68+
parser.add_argument("--bucket_name", help="Your Cloud Storage bucket name.")
69+
parser.add_argument("--object_name", help="Your Cloud Storage object name.")
70+
71+
args = parser.parse_args()
72+
73+
asyncio.run(
74+
storage_finalize_appendable_object_upload(
75+
bucket_name=args.bucket_name,
76+
object_name=args.object_name,
77+
)
78+
)

0 commit comments

Comments
 (0)