| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 6c16079 commit ea0f5bf
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -97,15 +97,29 @@ def __init__( | |||
| 97 | 97 | :param object_name: The name of the GCS Appendable Object to be written. | |
| 98 | 98 | ||
| 99 | 99 | :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. | ||
| 105 | 111 | ||
| 106 | 112 | :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`. | ||
| 109 | 123 | """ | |
| 110 | 124 | raise_if_no_fast_crc32c() | |
| 111 | 125 | self.client = client | |
@@ -133,7 +147,6 @@ def __init__( | |||
| 133 | 147 | self.flush_interval = writer_options.get( | |
| 134 | 148 | "FLUSH_INTERVAL_BYTES", _DEFAULT_FLUSH_INTERVAL_BYTES | |
| 135 | 149 | ) | |
| 136 | - # TODO: add test case for this. | ||
| 137 | 150 | if self.flush_interval < _MAX_CHUNK_SIZE_BYTES: | |
| 138 | 151 | raise exceptions.OutOfRange( | |
| 139 | 152 | 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: | |||
| 346 | 359 | self.offset = None | |
| 347 | 360 | return self.object_resource | |
| 348 | 361 | ||
| 362 | + @property | ||
| 363 | + def is_stream_open(self) -> bool: | ||
| 364 | + return self._is_stream_open | ||
| 365 | + | ||
| 366 | + | ||
| 349 | 367 | # helper methods. | |
| 350 | 368 | async def append_from_string(self, data: str): | |
| 351 | 369 | """ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -47,8 +47,17 @@ class _AsyncWriteObjectStream(_AsyncAbstractObjectStream): | |||
| 47 | 47 | :param object_name: The name of the GCS ``Appendable Object`` to be write. | |
| 48 | 48 | ||
| 49 | 49 | :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. | ||
| 52 | 61 | ||
| 53 | 62 | :type write_handle: bytes | |
| 54 | 63 | :param write_handle: (Optional) An existing handle for writing the object. | |
@@ -101,13 +110,16 @@ async def open(self) -> None: | |||
| 101 | 110 | # Create a new object or overwrite existing one if generation_number | |
| 102 | 111 | # is None. This makes it consistent with GCS JSON API behavior. | |
| 103 | 112 | # Created object type would be Appendable Object. | |
| 104 | - if self.generation_number is None: | ||
| 113 | + # if `generation_number` == 0 new object will be created only if there | ||
| 114 | + # isn't any existing object. | ||
| 115 | + if self.generation_number is None or self.generation_number == 0: | ||
| 105 | 116 | self.first_bidi_write_req = _storage_v2.BidiWriteObjectRequest( | |
| 106 | 117 | write_object_spec=_storage_v2.WriteObjectSpec( | |
| 107 | 118 | resource=_storage_v2.Object( | |
| 108 | 119 | name=self.object_name, bucket=self._full_bucket_name | |
| 109 | 120 | ), | |
| 110 | 121 | appendable=True, | |
| 122 | + if_generation_match=self.generation_number, | ||
| 111 | 123 | ), | |
| 112 | 124 | ) | |
| 113 | 125 | else: | |
@@ -118,7 +130,6 @@ async def open(self) -> None: | |||
| 118 | 130 | generation=self.generation_number, | |
| 119 | 131 | ), | |
| 120 | 132 | ) | |
| 121 | - | ||
| 122 | 133 | self.socket_like_rpc = AsyncBidiRpc( | |
| 123 | 134 | self.rpc, initial_request=self.first_bidi_write_req, metadata=self.metadata | |
| 124 | 135 | ) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -18,6 +18,7 @@ | |||
| 18 | 18 | from google.cloud.storage._experimental.asyncio.async_multi_range_downloader import ( | |
| 19 | 19 | AsyncMultiRangeDownloader, | |
| 20 | 20 | ) | |
| 21 | + from google.api_core.exceptions import FailedPrecondition | ||
| 21 | 22 | ||
| 22 | 23 | ||
| 23 | 24 | pytestmark = pytest.mark.skipif( | |
@@ -360,3 +361,74 @@ async def test_append_flushes_and_state_lookup(storage_client, blobs_to_delete): | |||
| 360 | 361 | await mrd.close() | |
| 361 | 362 | content = buffer.getvalue() | |
| 362 | 363 | assert content == full_data | |
| 364 | + | ||
| 365 | + @pytest.mark.asyncio | ||
| 366 | + async def test_open_with_generation_zero(storage_client, blobs_to_delete): | ||
| 367 | + """Tests that using `generation=0` fails if the object already exists. | ||
| 368 | + | ||
| 369 | + This test verifies that: | ||
| 370 | + 1. An object can be created using `AsyncAppendableObjectWriter` with `generation=0`. | ||
| 371 | + 2. Attempting to create the same object again with `generation=0` raises a | ||
| 372 | + `FailedPrecondition` error with a 400 status code, because the | ||
| 373 | + precondition (object must not exist) is not met. | ||
| 374 | + """ | ||
| 375 | + object_name = f"test_append_with_generation-{uuid.uuid4()}" | ||
| 376 | + grpc_client = AsyncGrpcClient().grpc_client | ||
| 377 | + writer = AsyncAppendableObjectWriter(grpc_client, _ZONAL_BUCKET, object_name, generation=0) | ||
| 378 | + | ||
| 379 | + # Empty object is created. | ||
| 380 | + await writer.open() | ||
| 381 | + assert writer.is_stream_open | ||
| 382 | + | ||
| 383 | + await writer.close() | ||
| 384 | + assert not writer.is_stream_open | ||
| 385 | + | ||
| 386 | + | ||
| 387 | + with pytest.raises(FailedPrecondition) as exc_info: | ||
| 388 | + writer = AsyncAppendableObjectWriter( | ||
| 389 | + grpc_client, _ZONAL_BUCKET, object_name, generation=0 | ||
| 390 | + ) | ||
| 391 | + await writer.open() | ||
| 392 | + assert exc_info.value.code == 400 | ||
| 393 | + | ||
| 394 | + # cleanup | ||
| 395 | + del writer | ||
| 396 | + gc.collect() | ||
| 397 | + | ||
| 398 | + blobs_to_delete.append(storage_client.bucket(_ZONAL_BUCKET).blob(object_name)) | ||
| 399 | + | ||
| 400 | + @pytest.mark.asyncio | ||
| 401 | + async def test_open_existing_object_with_gen_None_overrides_existing(storage_client, blobs_to_delete): | ||
| 402 | + """ | ||
| 403 | + Test that a new writer when specifies `None` overrides the existing object. | ||
| 404 | + """ | ||
| 405 | + object_name = f"test_append_with_generation-{uuid.uuid4()}" | ||
| 406 | + | ||
| 407 | + grpc_client = AsyncGrpcClient().grpc_client | ||
| 408 | + writer = AsyncAppendableObjectWriter(grpc_client, _ZONAL_BUCKET, object_name, generation=0) | ||
| 409 | + | ||
| 410 | + # Empty object is created. | ||
| 411 | + await writer.open() | ||
| 412 | + assert writer.is_stream_open | ||
| 413 | + old_gen = writer.generation | ||
| 414 | + | ||
| 415 | + | ||
| 416 | + await writer.close() | ||
| 417 | + assert not writer.is_stream_open | ||
| 418 | + | ||
| 419 | + | ||
| 420 | + | ||
| 421 | + new_writer = AsyncAppendableObjectWriter( | ||
| 422 | + grpc_client, _ZONAL_BUCKET, object_name, generation=None | ||
| 423 | + ) | ||
| 424 | + await new_writer.open() | ||
| 425 | + assert new_writer.generation != old_gen | ||
| 426 | + | ||
| 427 | + # assert exc_info.value.code == 400 | ||
| 428 | + | ||
| 429 | + # cleanup | ||
| 430 | + del writer | ||
| 431 | + del new_writer | ||
| 432 | + gc.collect() | ||
| 433 | + | ||
| 434 | + blobs_to_delete.append(storage_client.bucket(_ZONAL_BUCKET).blob(object_name)) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -55,7 +55,7 @@ def test_init(mock_write_object_stream, mock_client): | |||
| 55 | 55 | assert writer.object_name == OBJECT | |
| 56 | 56 | assert writer.generation is None | |
| 57 | 57 | assert writer.write_handle is None | |
| 58 | - assert not writer._is_stream_open | ||
| 58 | + assert not writer.is_stream_open | ||
| 59 | 59 | assert writer.offset is None | |
| 60 | 60 | assert writer.persisted_size is None | |
| 61 | 61 | assert writer.bytes_appended_since_last_flush == 0 | |
@@ -225,7 +225,7 @@ async def test_open_appendable_object_writer(mock_write_object_stream, mock_clie | |||
| 225 | 225 | ||
| 226 | 226 | # Assert | |
| 227 | 227 | mock_stream.open.assert_awaited_once() | |
| 228 | - assert writer._is_stream_open | ||
| 228 | + assert writer.is_stream_open | ||
| 229 | 229 | assert writer.generation == GENERATION | |
| 230 | 230 | assert writer.write_handle == WRITE_HANDLE | |
| 231 | 231 | assert writer.persisted_size == 0 | |
@@ -255,7 +255,7 @@ async def test_open_appendable_object_writer_existing_object( | |||
| 255 | 255 | ||
| 256 | 256 | # Assert | |
| 257 | 257 | mock_stream.open.assert_awaited_once() | |
| 258 | - assert writer._is_stream_open | ||
| 258 | + assert writer.is_stream_open | ||
| 259 | 259 | assert writer.generation == GENERATION | |
| 260 | 260 | assert writer.write_handle == WRITE_HANDLE | |
| 261 | 261 | assert writer.persisted_size == PERSISTED_SIZE | |
@@ -379,7 +379,7 @@ async def test_close(mock_write_object_stream, mock_client): | |||
| 379 | 379 | mock_stream.close.assert_awaited_once() | |
| 380 | 380 | assert writer.offset is None | |
| 381 | 381 | assert persisted_size == 1024 | |
| 382 | - assert not writer._is_stream_open | ||
| 382 | + assert not writer.is_stream_open | ||
| 383 | 383 | ||
| 384 | 384 | ||
| 385 | 385 | @pytest.mark.asyncio | |
@@ -415,7 +415,7 @@ async def test_finalize_on_close(mock_write_object_stream, mock_client): | |||
| 415 | 415 | ||
| 416 | 416 | # Assert | |
| 417 | 417 | mock_stream.close.assert_awaited_once() | |
| 418 | - assert not writer._is_stream_open | ||
| 418 | + assert not writer.is_stream_open | ||
| 419 | 419 | assert writer.offset is None | |
| 420 | 420 | assert writer.object_resource == mock_resource | |
| 421 | 421 | assert writer.persisted_size == 2048 | |
@@ -448,7 +448,7 @@ async def test_finalize(mock_write_object_stream, mock_client): | |||
| 448 | 448 | assert writer.object_resource == mock_resource | |
| 449 | 449 | assert writer.persisted_size == 123 | |
| 450 | 450 | assert gcs_object == mock_resource | |
| 451 | - assert writer._is_stream_open is False | ||
| 451 | + assert not writer.is_stream_open | ||
| 452 | 452 | assert writer.offset is None | |
| 453 | 453 | ||
| 454 | 454 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -148,6 +148,42 @@ async def test_open_for_new_object(mock_async_bidi_rpc, mock_client): | |||
| 148 | 148 | assert stream.persisted_size == 0 | |
| 149 | 149 | ||
| 150 | 150 | ||
| 151 | + @pytest.mark.asyncio | ||
| 152 | + @mock.patch( | ||
| 153 | + "google.cloud.storage._experimental.asyncio.async_write_object_stream.AsyncBidiRpc" | ||
| 154 | + ) | ||
| 155 | + async def test_open_for_new_object_with_generation_zero(mock_async_bidi_rpc, mock_client): | ||
| 156 | + """Test opening a stream for a new object.""" | ||
| 157 | + # Arrange | ||
| 158 | + socket_like_rpc = mock.AsyncMock() | ||
| 159 | + mock_async_bidi_rpc.return_value = socket_like_rpc | ||
| 160 | + socket_like_rpc.open = mock.AsyncMock() | ||
| 161 | + | ||
| 162 | + mock_response = mock.MagicMock(spec=_storage_v2.BidiWriteObjectResponse) | ||
| 163 | + mock_response.resource = mock.MagicMock(spec=_storage_v2.Object) | ||
| 164 | + mock_response.resource.generation = GENERATION | ||
| 165 | + mock_response.resource.size = 0 | ||
| 166 | + mock_response.write_handle = WRITE_HANDLE | ||
| 167 | + socket_like_rpc.recv = mock.AsyncMock(return_value=mock_response) | ||
| 168 | + | ||
| 169 | + stream = _AsyncWriteObjectStream(mock_client, BUCKET, OBJECT, generation_number=0) | ||
| 170 | + | ||
| 171 | + # Act | ||
| 172 | + await stream.open() | ||
| 173 | + | ||
| 174 | + # Assert | ||
| 175 | + mock_async_bidi_rpc.assert_called_once() | ||
| 176 | + _, call_kwargs = mock_async_bidi_rpc.call_args | ||
| 177 | + initial_request = call_kwargs["initial_request"] | ||
| 178 | + assert initial_request.write_object_spec.if_generation_match == 0 | ||
| 179 | + assert stream._is_stream_open | ||
| 180 | + socket_like_rpc.open.assert_called_once() | ||
| 181 | + socket_like_rpc.recv.assert_called_once() | ||
| 182 | + assert stream.generation_number == GENERATION | ||
| 183 | + assert stream.write_handle == WRITE_HANDLE | ||
| 184 | + assert stream.persisted_size == 0 | ||
| 185 | + | ||
| 186 | + | ||
| 151 | 187 | @pytest.mark.asyncio | |
| 152 | 188 | @mock.patch( | |
| 153 | 189 | "google.cloud.storage._experimental.asyncio.async_write_object_stream.AsyncBidiRpc" | |
| Back | FazBrowse Home | New Git URL |
0 commit comments