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

Commit 1c6b76f

Browse files
authored
feat(zb-experimental): implement append (#1620)
feat(zb-experimental): implement append
1 parent 8c25c1b commit 1c6b76f

2 files changed

Lines changed: 310 additions & 21 deletions

File tree

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

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
if you want to use these Rapid Storage APIs.
2222
2323
"""
24-
fromtypingimportOptional
24+
fromtypingimportOptional, Union
2525
fromgoogle.cloudimport_storage_v2
2626
fromgoogle.cloud.storage._experimental.asyncio.async_grpc_clientimport (
2727
AsyncGrpcClient,
@@ -31,6 +31,10 @@
3131
)
3232

3333

34+
_MAX_CHUNK_SIZE_BYTES=2*1024*1024# 2 MiB
35+
_MAX_BUFFER_SIZE_BYTES=16*1024*1024# 16 MiB
36+
37+
3438
classAsyncAppendableObjectWriter:
3539
"""Class for appending data to a GCS Appendable Object asynchronously."""
3640

@@ -118,7 +122,13 @@ async def state_lookup(self) -> int:
118122
119123
:rtype: int
120124
:returns: persisted size.
125+
126+
:raises ValueError: If the stream is not open (i.e., `open()` has not
127+
been called).
121128
"""
129+
ifnotself._is_stream_open:
130+
raiseValueError("Stream is not open. Call open() before state_lookup().")
131+
122132
awaitself.write_obj_stream.send(
123133
_storage_v2.BidiWriteObjectRequest(
124134
state_lookup=True,
@@ -129,7 +139,11 @@ async def state_lookup(self) -> int:
129139
returnself.persisted_size
130140

131141
asyncdefopen(self) ->None:
132-
"""Opens the underlying bidi-gRPC stream."""
142+
"""Opens the underlying bidi-gRPC stream.
143+
144+
:raises ValueError: If the stream is already open.
145+
146+
"""
133147
ifself._is_stream_open:
134148
raiseValueError("Underlying bidi-gRPC stream is already open")
135149

@@ -142,15 +156,65 @@ async def open(self) -> None:
142156
# Update self.persisted_size
143157
_=awaitself.state_lookup()
144158

145-
asyncdefappend(self, data: bytes):
146-
raiseNotImplementedError("append is not implemented yet.")
159+
asyncdefappend(self, data: bytes) ->None:
160+
"""Appends data to the Appendable object.
161+
162+
This method sends the provided data to the GCS server in chunks. It
163+
maintains an internal threshold `_MAX_BUFFER_SIZE_BYTES` and will
164+
automatically flush the data to make it visible to readers when that
165+
threshold has reached.
166+
167+
:type data: bytes
168+
:param data: The bytes to append to the object.
169+
170+
:rtype: None
171+
172+
:raises ValueError: If the stream is not open (i.e., `open()` has not
173+
been called).
174+
"""
175+
176+
ifnotself._is_stream_open:
177+
raiseValueError("Stream is not open. Call open() before append().")
178+
total_bytes=len(data)
179+
iftotal_bytes==0:
180+
# TODO: add warning.
181+
return
182+
ifself.offsetisNone:
183+
assertself.persisted_sizeisnotNone
184+
self.offset=self.persisted_size
185+
186+
start_idx=0
187+
bytes_to_flush=0
188+
whilestart_idx<total_bytes:
189+
end_idx=min(start_idx+_MAX_CHUNK_SIZE_BYTES, total_bytes)
190+
awaitself.write_obj_stream.send(
191+
_storage_v2.BidiWriteObjectRequest(
192+
write_offset=self.offset,
193+
checksummed_data=_storage_v2.ChecksummedData(
194+
content=data[start_idx:end_idx]
195+
),
196+
)
197+
)
198+
chunk_size=end_idx-start_idx
199+
self.offset+=chunk_size
200+
bytes_to_flush+=chunk_size
201+
ifbytes_to_flush>=_MAX_BUFFER_SIZE_BYTES:
202+
awaitself.flush()
203+
bytes_to_flush=0
204+
start_idx=end_idx
147205

148206
asyncdefflush(self) ->int:
149207
"""Flushes the data to the server.
150208
151209
:rtype: int
152210
:returns: The persisted size after flush.
211+
212+
:raises ValueError: If the stream is not open (i.e., `open()` has not
213+
been called).
153214
"""
215+
ifnotself._is_stream_open:
216+
raiseValueError("Stream is not open. Call open() before flush().")
217+
154218
awaitself.write_obj_stream.send(
155219
_storage_v2.BidiWriteObjectRequest(
156220
flush=True,
@@ -162,14 +226,34 @@ async def flush(self) -> int:
162226
self.offset=self.persisted_size
163227
returnself.persisted_size
164228

165-
asyncdefclose(self, finalize_on_close=False) ->int:
166-
"""Returns persisted_size"""
229+
asyncdefclose(self, finalize_on_close=False) ->Union[int, _storage_v2.Object]:
230+
"""Closes the underlying bidi-gRPC stream.
231+
232+
:type finalize_on_close: bool
233+
:param finalize_on_close: Finalizes the Appendable Object. No more data
234+
can be appended.
235+
236+
rtype: Union[int, _storage_v2.Object]
237+
returns: Updated `self.persisted_size` by default after closing the
238+
bidi-gRPC stream. However, if `finalize_on_close=True` is passed,
239+
returns the finalized object resource.
240+
241+
:raises ValueError: If the stream is not open (i.e., `open()` has not
242+
been called).
243+
244+
"""
245+
ifnotself._is_stream_open:
246+
raiseValueError("Stream is not open. Call open() before close().")
247+
167248
iffinalize_on_close:
168249
awaitself.finalize()
250+
else:
251+
awaitself.flush()
252+
awaitself.write_obj_stream.close()
169253

170-
awaitself.write_obj_stream.close()
171254
self._is_stream_open=False
172255
self.offset=None
256+
returnself.object_resourceiffinalize_on_closeelseself.persisted_size
173257

174258
asyncdeffinalize(self) ->_storage_v2.Object:
175259
"""Finalizes the Appendable Object.
@@ -178,12 +262,20 @@ async def finalize(self) -> _storage_v2.Object:
178262
179263
rtype: google.cloud.storage_v2.types.Object
180264
returns: The finalized object resource.
265+
266+
:raises ValueError: If the stream is not open (i.e., `open()` has not
267+
been called).
181268
"""
269+
ifnotself._is_stream_open:
270+
raiseValueError("Stream is not open. Call open() before finalize().")
271+
182272
awaitself.write_obj_stream.send(
183273
_storage_v2.BidiWriteObjectRequest(finish_write=True)
184274
)
185275
response=awaitself.write_obj_stream.recv()
186276
self.object_resource=response.resource
277+
self.persisted_size=self.object_resource.size
278+
returnself.object_resource
187279

188280
# helper methods.
189281
asyncdefappend_from_string(self, data: str):

0 commit comments

Comments
 (0)