Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@

[1]: https://pypi.org/project/google-cloud-storage/#history

## [3.5.0](https://github.com/googleapis/python-storage/compare/v3.4.1...v3.5.0) (2025-11-05)


### Features

* **experimental:** Add base resumption strategy for bidi streams ([#1594](https://github.com/googleapis/python-storage/issues/1594)) ([5fb85ea](https://github.com/googleapis/python-storage/commit/5fb85ea544dcc9ed9dca65957c872c3811f02b87))
* **experimental:** Add checksum for bidi reads operation ([#1566](https://github.com/googleapis/python-storage/issues/1566)) ([93ce515](https://github.com/googleapis/python-storage/commit/93ce515d60f0ac77ab83680ba2b4d6a9f57e75d0))
* **experimental:** Add read resumption strategy ([#1599](https://github.com/googleapis/python-storage/issues/1599)) ([5d5e895](https://github.com/googleapis/python-storage/commit/5d5e895e173075da557b58614fecc84086aaf9cb))
* **experimental:** Handle BidiReadObjectRedirectedError for bidi reads ([#1600](https://github.com/googleapis/python-storage/issues/1600)) ([71b0f8a](https://github.com/googleapis/python-storage/commit/71b0f8a368a61bed9bd793a059f980562061223e))
* Indicate that md5 is used as a CRC ([#1522](https://github.com/googleapis/python-storage/issues/1522)) ([961536c](https://github.com/googleapis/python-storage/commit/961536c7bf3652a824c207754317030526b9dd28))
* Provide option to update user_agent ([#1596](https://github.com/googleapis/python-storage/issues/1596)) ([02f1451](https://github.com/googleapis/python-storage/commit/02f1451aaa8dacd10a862e97abb62ae48249b9b4))


### Bug Fixes

* Deprecate credentials_file argument ([74415a2](https://github.com/googleapis/python-storage/commit/74415a2a120e9bfa42f4f5fc8bd2f8e0d4cf5d18))
* Flaky system tests for resumable_media ([#1592](https://github.com/googleapis/python-storage/issues/1592)) ([7fee3dd](https://github.com/googleapis/python-storage/commit/7fee3dd3390cfb5475a39d8f8272ea825dbda449))
* Make `download_ranges` compatible with `asyncio.create_task(..)` ([#1591](https://github.com/googleapis/python-storage/issues/1591)) ([faf8b83](https://github.com/googleapis/python-storage/commit/faf8b83b1f0ac378f8f6f47ce33dc23a866090c9))
* Make `download_ranges` compatible with `asyncio.create_task(..)` ([#1591](https://github.com/googleapis/python-storage/issues/1591)) ([faf8b83](https://github.com/googleapis/python-storage/commit/faf8b83b1f0ac378f8f6f47ce33dc23a866090c9))
* Redact sensitive data from OTEL traces and fix env var parsing ([#1553](https://github.com/googleapis/python-storage/issues/1553)) ([a38ca19](https://github.com/googleapis/python-storage/commit/a38ca1977694def98f65ae7239e300a987bbd262))
* Redact sensitive data from OTEL traces and fix env var parsing ([#1553](https://github.com/googleapis/python-storage/issues/1553)) ([a38ca19](https://github.com/googleapis/python-storage/commit/a38ca1977694def98f65ae7239e300a987bbd262))
* Use separate header object for each upload in Transfer Manager MPU ([#1595](https://github.com/googleapis/python-storage/issues/1595)) ([0d867bd](https://github.com/googleapis/python-storage/commit/0d867bd4f405d2dbeca1edfc8072080c5a96c1cd))

## [3.4.1](https://github.com/googleapis/python-storage/compare/v3.4.0...v3.5.0) (2025-10-08)

### Bug Fixes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import abc
from typing import Any, Iterable


class _BaseResumptionStrategy(abc.ABC):
"""Abstract base class defining the interface for a bidi stream resumption strategy.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@
)
from google.cloud._storage_v2.types.storage import BidiReadObjectRedirectedError


class _DownloadState:
"""A helper class to track the state of a single range download."""
def __init__(self, initial_offset: int, initial_length: int, user_buffer: IO[bytes]):

def __init__(
self, initial_offset: int, initial_length: int, user_buffer: IO[bytes]
):
self.initial_offset = initial_offset
self.initial_length = initial_length
self.user_buffer = user_buffer
Expand Down Expand Up @@ -42,7 +46,9 @@ def generate_requests(self, state: dict) -> List[storage_v2.ReadRange]:
pending_requests.append(new_request)
return pending_requests

def update_state_from_response(self, response: storage_v2.BidiReadObjectResponse, state: dict) -> None:
def update_state_from_response(
self, response: storage_v2.BidiReadObjectResponse, state: dict
) -> None:
"""Processes a server response, performs integrity checks, and updates state."""
for object_data_range in response.object_data_ranges:
read_id = object_data_range.read_range.read_id
Expand All @@ -62,13 +68,18 @@ def update_state_from_response(self, response: storage_v2.BidiReadObjectResponse
# Final Byte Count Verification
if object_data_range.range_end:
read_state.is_complete = True
if read_state.initial_length != 0 and read_state.bytes_written != read_state.initial_length:
raise DataCorruption(response, f"Byte count mismatch for read_id {read_id}")
if (
read_state.initial_length != 0
and read_state.bytes_written != read_state.initial_length
):
raise DataCorruption(
response, f"Byte count mismatch for read_id {read_id}"
)

async def recover_state_on_failure(self, error: Exception, state: Any) -> None:
"""Handles BidiReadObjectRedirectedError for reads."""
# This would parse the gRPC error details, extract the routing_token,
# and store it on the shared state object.
cause = getattr(error, "cause", error)
if isinstance(cause, BidiReadObjectRedirectedError):
state['routing_token'] = cause.routing_token
state["routing_token"] = cause.routing_token
2 changes: 1 addition & 1 deletion google/cloud/storage/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.

__version__ = "3.4.1"
__version__ = "3.5.0"
24 changes: 16 additions & 8 deletions tests/unit/asyncio/retry/test_reads_resumption_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ def test_update_state_processes_single_chunk_successfully(self):
response = storage_v2.BidiReadObjectResponse(
object_data_ranges=[
storage_v2.types.ObjectRangeData(
read_range=storage_v2.ReadRange(read_id=_READ_ID, read_offset=0, read_length=len(data)),
read_range=storage_v2.ReadRange(
read_id=_READ_ID, read_offset=0, read_length=len(data)
),
checksummed_data=storage_v2.ChecksummedData(content=data),
)
]
Expand All @@ -130,7 +132,9 @@ def test_update_state_from_response_offset_mismatch(self):
response = storage_v2.BidiReadObjectResponse(
object_data_ranges=[
storage_v2.types.ObjectRangeData(
read_range=storage_v2.ReadRange(read_id=_READ_ID, read_offset=0, read_length=4),
read_range=storage_v2.ReadRange(
read_id=_READ_ID, read_offset=0, read_length=4
),
checksummed_data=storage_v2.ChecksummedData(content=b"data"),
)
]
Expand All @@ -149,7 +153,9 @@ def test_update_state_from_response_final_byte_count_mismatch(self):
response = storage_v2.BidiReadObjectResponse(
object_data_ranges=[
storage_v2.types.ObjectRangeData(
read_range=storage_v2.ReadRange(read_id=_READ_ID, read_offset=0, read_length=4),
read_range=storage_v2.ReadRange(
read_id=_READ_ID, read_offset=0, read_length=4
),
checksummed_data=storage_v2.ChecksummedData(content=b"data"),
range_end=True,
)
Expand All @@ -171,7 +177,9 @@ def test_update_state_from_response_completes_download(self):
response = storage_v2.BidiReadObjectResponse(
object_data_ranges=[
storage_v2.types.ObjectRangeData(
read_range=storage_v2.ReadRange(read_id=_READ_ID, read_offset=0, read_length=len(data)),
read_range=storage_v2.ReadRange(
read_id=_READ_ID, read_offset=0, read_length=len(data)
),
checksummed_data=storage_v2.ChecksummedData(content=data),
range_end=True,
)
Expand All @@ -195,7 +203,9 @@ def test_update_state_from_response_completes_download_zero_length(self):
response = storage_v2.BidiReadObjectResponse(
object_data_ranges=[
storage_v2.types.ObjectRangeData(
read_range=storage_v2.ReadRange(read_id=_READ_ID, read_offset=0, read_length=len(data)),
read_range=storage_v2.ReadRange(
read_id=_READ_ID, read_offset=0, read_length=len(data)
),
checksummed_data=storage_v2.ChecksummedData(content=data),
range_end=True,
)
Expand All @@ -215,9 +225,7 @@ async def test_recover_state_on_failure_handles_redirect(self):
self.assertIsNone(state.get("routing_token"))

dummy_token = "dummy-routing-token"
redirect_error = BidiReadObjectRedirectedError(
routing_token=dummy_token
)
redirect_error = BidiReadObjectRedirectedError(routing_token=dummy_token)

final_error = exceptions.RetryError("Retry failed", cause=redirect_error)

Expand Down