google-cloud-storage 3.3.0__tar.gz → 3.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. {google_cloud_storage-3.3.0/google_cloud_storage.egg-info → google_cloud_storage-3.3.1}/PKG-INFO +1 -1
  2. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_media/_upload.py +41 -3
  3. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_media/requests/upload.py +9 -0
  4. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/blob.py +49 -11
  5. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/bucket.py +23 -3
  6. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/version.py +1 -1
  7. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1/google_cloud_storage.egg-info}/PKG-INFO +1 -1
  8. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google_cloud_storage.egg-info/SOURCES.txt +1 -0
  9. google_cloud_storage-3.3.1/tests/data/random_9_MiB_file +0 -0
  10. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/unit/requests/test_upload.py +65 -0
  11. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/unit/test__upload.py +28 -3
  12. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/system/conftest.py +1 -0
  13. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/system/test__signing.py +13 -39
  14. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/system/test_blob.py +56 -24
  15. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test_blob.py +25 -1
  16. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/LICENSE +0 -0
  17. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/MANIFEST.in +0 -0
  18. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/README.rst +0 -0
  19. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/__init__.py +0 -0
  20. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_helpers.py +0 -0
  21. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_http.py +0 -0
  22. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_media/__init__.py +0 -0
  23. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_media/_download.py +0 -0
  24. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_media/_helpers.py +0 -0
  25. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_media/common.py +0 -0
  26. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_media/py.typed +0 -0
  27. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_media/requests/__init__.py +0 -0
  28. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_media/requests/_request_helpers.py +0 -0
  29. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_media/requests/download.py +0 -0
  30. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_opentelemetry_tracing.py +0 -0
  31. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/_signing.py +0 -0
  32. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/acl.py +0 -0
  33. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/batch.py +0 -0
  34. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/client.py +0 -0
  35. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/constants.py +0 -0
  36. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/exceptions.py +0 -0
  37. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/fileio.py +0 -0
  38. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/hmac_key.py +0 -0
  39. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/iam.py +0 -0
  40. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/ip_filter.py +0 -0
  41. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/notification.py +0 -0
  42. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/retry.py +0 -0
  43. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage/transfer_manager.py +0 -0
  44. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/__init__.py +0 -0
  45. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/gapic_metadata.json +0 -0
  46. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/gapic_version.py +0 -0
  47. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/py.typed +0 -0
  48. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/services/__init__.py +0 -0
  49. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/services/storage/__init__.py +0 -0
  50. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/services/storage/async_client.py +0 -0
  51. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/services/storage/client.py +0 -0
  52. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/services/storage/pagers.py +0 -0
  53. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/services/storage/transports/__init__.py +0 -0
  54. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/services/storage/transports/base.py +0 -0
  55. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/services/storage/transports/grpc.py +0 -0
  56. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/services/storage/transports/grpc_asyncio.py +0 -0
  57. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/types/__init__.py +0 -0
  58. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google/cloud/storage_v2/types/storage.py +0 -0
  59. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google_cloud_storage.egg-info/dependency_links.txt +0 -0
  60. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google_cloud_storage.egg-info/not-zip-safe +0 -0
  61. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google_cloud_storage.egg-info/requires.txt +0 -0
  62. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/google_cloud_storage.egg-info/top_level.txt +0 -0
  63. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/setup.cfg +0 -0
  64. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/setup.py +0 -0
  65. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/__init__.py +0 -0
  66. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/conformance/README.md +0 -0
  67. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/conformance/__init__.py +0 -0
  68. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/conformance/conftest.py +0 -0
  69. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/conformance/retry_strategy_test_data.json +0 -0
  70. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/conformance/test_conformance.py +0 -0
  71. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/data/CloudPlatform_128px_Retina.png +0 -0
  72. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/data/five-point-one-mb-file.zip +0 -0
  73. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/data/simple.txt +0 -0
  74. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/perf/README.md +0 -0
  75. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/perf/_perf_utils.py +0 -0
  76. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/perf/benchmarking.py +0 -0
  77. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/perf/profile_w1r3.py +0 -0
  78. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/__init__.py +0 -0
  79. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/data/brotli.txt +0 -0
  80. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/data/brotli.txt.br +0 -0
  81. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/data/favicon.ico +0 -0
  82. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/data/file.txt +0 -0
  83. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/data/gzipped.txt +0 -0
  84. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/data/gzipped.txt.gz +0 -0
  85. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/data/image1.jpg +0 -0
  86. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/data/image2.jpg +0 -0
  87. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/system/__init__.py +0 -0
  88. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/system/credentials.json.enc +0 -0
  89. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/system/requests/__init__.py +0 -0
  90. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/system/requests/conftest.py +0 -0
  91. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/system/requests/test_download.py +0 -0
  92. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/system/requests/test_upload.py +0 -0
  93. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/system/utils.py +0 -0
  94. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/unit/__init__.py +0 -0
  95. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/unit/requests/__init__.py +0 -0
  96. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/unit/requests/test__helpers.py +0 -0
  97. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/unit/requests/test_download.py +0 -0
  98. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/unit/test__download.py +0 -0
  99. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/resumable_media/unit/test__helpers.py +0 -0
  100. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/system/__init__.py +0 -0
  101. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/system/_helpers.py +0 -0
  102. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/system/test_bucket.py +1 -1
  103. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/system/test_client.py +0 -0
  104. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/system/test_fileio.py +0 -0
  105. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/system/test_hmac_key_metadata.py +0 -0
  106. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/system/test_kms_integration.py +0 -0
  107. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/system/test_notification.py +0 -0
  108. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/system/test_transfer_manager.py +0 -0
  109. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/__init__.py +0 -0
  110. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/gapic/__init__.py +0 -0
  111. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/gapic/storage_v2/__init__.py +0 -0
  112. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/gapic/storage_v2/test_storage.py +0 -0
  113. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test__helpers.py +0 -0
  114. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test__http.py +0 -0
  115. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test__opentelemetry_tracing.py +0 -0
  116. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test__signing.py +0 -0
  117. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test_acl.py +0 -0
  118. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test_batch.py +0 -0
  119. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test_bucket.py +0 -0
  120. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test_client.py +0 -0
  121. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test_exceptions.py +0 -0
  122. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test_fileio.py +0 -0
  123. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test_hmac_key.py +0 -0
  124. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test_ip_filter.py +0 -0
  125. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test_notification.py +0 -0
  126. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test_retry.py +0 -0
  127. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/test_transfer_manager.py +0 -0
  128. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/url_signer_v4_test_account.json +0 -0
  129. {google_cloud_storage-3.3.0 → google_cloud_storage-3.3.1}/tests/unit/url_signer_v4_test_data.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-cloud-storage
3
- Version: 3.3.0
3
+ Version: 3.3.1
4
4
  Summary: Google Cloud Storage API client library
5
5
  Home-page: https://github.com/googleapis/python-storage
6
6
  Author: Google LLC
@@ -411,7 +411,12 @@ class ResumableUpload(UploadBase):
411
411
  """
412
412
 
413
413
  def __init__(
414
- self, upload_url, chunk_size, checksum="auto", headers=None, retry=DEFAULT_RETRY
414
+ self,
415
+ upload_url,
416
+ chunk_size,
417
+ checksum="auto",
418
+ headers=None,
419
+ retry=DEFAULT_RETRY,
415
420
  ):
416
421
  super(ResumableUpload, self).__init__(upload_url, headers=headers, retry=retry)
417
422
  if chunk_size % UPLOAD_CHUNK_SIZE != 0:
@@ -472,7 +477,12 @@ class ResumableUpload(UploadBase):
472
477
  return self._total_bytes
473
478
 
474
479
  def _prepare_initiate_request(
475
- self, stream, metadata, content_type, total_bytes=None, stream_final=True
480
+ self,
481
+ stream,
482
+ metadata,
483
+ content_type,
484
+ total_bytes=None,
485
+ stream_final=True,
476
486
  ):
477
487
  """Prepare the contents of HTTP request to initiate upload.
478
488
 
@@ -955,7 +965,12 @@ class XMLMPUContainer(UploadBase):
955
965
  """
956
966
 
957
967
  def __init__(
958
- self, upload_url, filename, headers=None, upload_id=None, retry=DEFAULT_RETRY
968
+ self,
969
+ upload_url,
970
+ filename,
971
+ headers=None,
972
+ upload_id=None,
973
+ retry=DEFAULT_RETRY,
959
974
  ):
960
975
  super().__init__(upload_url, headers=headers, retry=retry)
961
976
  self._filename = filename
@@ -1372,6 +1387,29 @@ class XMLMPUPart(UploadBase):
1372
1387
 
1373
1388
  .. _sans-I/O: https://sans-io.readthedocs.io/
1374
1389
  """
1390
+ # Data corruption errors shouldn't be considered as invalid responses,
1391
+ # So we handle them earlier than call to `_helpers.require_status_code`.
1392
+ # If the response is 400, we check for data corruption errors.
1393
+ if response.status_code == 400:
1394
+ root = ElementTree.fromstring(response.text)
1395
+ error_code = root.find("Code").text
1396
+ error_message = root.find("Message").text
1397
+ error_details = root.find("Details").text
1398
+ if error_code in ["InvalidDigest", "BadDigest", "CrcMismatch"]:
1399
+ raise DataCorruption(
1400
+ response,
1401
+ (
1402
+ "Checksum mismatch: checksum calculated by client and"
1403
+ " server did not match. Error code: {error_code},"
1404
+ " Error message: {error_message},"
1405
+ " Error details: {error_details}"
1406
+ ).format(
1407
+ error_code=error_code,
1408
+ error_message=error_message,
1409
+ error_details=error_details,
1410
+ ),
1411
+ )
1412
+
1375
1413
  _helpers.require_status_code(
1376
1414
  response,
1377
1415
  (http.client.OK,),
@@ -21,6 +21,7 @@ uploads that contain both metadata and a small file as payload.
21
21
 
22
22
  from google.cloud.storage._media import _upload
23
23
  from google.cloud.storage._media.requests import _request_helpers
24
+ from google.cloud.storage._media import _helpers
24
25
 
25
26
 
26
27
  class SimpleUpload(_request_helpers.RequestsMixin, _upload.SimpleUpload):
@@ -757,6 +758,14 @@ class XMLMPUPart(_request_helpers.RequestsMixin, _upload.XMLMPUPart):
757
758
  ~requests.Response: The HTTP response returned by ``transport``.
758
759
  """
759
760
  method, url, payload, headers = self._prepare_upload_request()
761
+ if self._checksum_object is not None:
762
+ checksum_digest_in_base64 = _helpers.prepare_checksum_digest(
763
+ self._checksum_object.digest()
764
+ )
765
+ if self._checksum_type == "crc32c":
766
+ headers["X-Goog-Hash"] = f"crc32c={checksum_digest_in_base64}"
767
+ elif self._checksum_type == "md5":
768
+ headers["X-Goog-Hash"] = f"md5={checksum_digest_in_base64}"
760
769
 
761
770
  # Wrap the request business logic in a function to be retried.
762
771
  def retriable_request():
@@ -14,8 +14,7 @@
14
14
 
15
15
  # pylint: disable=too-many-lines
16
16
 
17
- """Create / interact with Google Cloud Storage blobs.
18
- """
17
+ """Create / interact with Google Cloud Storage blobs."""
19
18
 
20
19
  import base64
21
20
  import copy
@@ -142,8 +141,8 @@ _GS_URL_REGEX_PATTERN = re.compile(
142
141
  r"(?P<scheme>gs)://(?P<bucket_name>[a-z0-9_.-]+)/(?P<object_name>.+)"
143
142
  )
144
143
 
145
- _DEFAULT_CHUNKSIZE = 104857600 # 1024 * 1024 B * 100 = 100 MB
146
- _MAX_MULTIPART_SIZE = 8388608 # 8 MB
144
+ _DEFAULT_CHUNKSIZE = 104857600 # 1024 * 1024 B * 100 = 100 MiB
145
+ _MAX_MULTIPART_SIZE = 8388608 # 8 MiB
147
146
 
148
147
  _logger = logging.getLogger(__name__)
149
148
 
@@ -181,6 +180,14 @@ class Blob(_PropertyMixin):
181
180
  :type generation: long
182
181
  :param generation:
183
182
  (Optional) If present, selects a specific revision of this object.
183
+
184
+ :type crc32c_checksum: str
185
+ :param crc32c_checksum:
186
+ (Optional) If set, the CRC32C checksum of the blob's content.
187
+ CRC32c checksum, as described in RFC 4960, Appendix B; encoded using
188
+ base64 in big-endian byte order. See
189
+ Apenndix B: https://datatracker.ietf.org/doc/html/rfc4960#appendix-B
190
+ base64: https://datatracker.ietf.org/doc/html/rfc4648#section-4
184
191
  """
185
192
 
186
193
  _chunk_size = None # Default value for each instance.
@@ -214,6 +221,7 @@ class Blob(_PropertyMixin):
214
221
  encryption_key=None,
215
222
  kms_key_name=None,
216
223
  generation=None,
224
+ crc32c_checksum=None,
217
225
  ):
218
226
  """
219
227
  property :attr:`name`
@@ -237,6 +245,9 @@ class Blob(_PropertyMixin):
237
245
  if generation is not None:
238
246
  self._properties["generation"] = generation
239
247
 
248
+ if crc32c_checksum is not None:
249
+ self._properties["crc32c"] = crc32c_checksum
250
+
240
251
  @property
241
252
  def bucket(self):
242
253
  """Bucket which contains the object.
@@ -1643,7 +1654,9 @@ class Blob(_PropertyMixin):
1643
1654
  :raises: :class:`google.cloud.exceptions.NotFound`
1644
1655
  """
1645
1656
  warnings.warn(
1646
- _DOWNLOAD_AS_STRING_DEPRECATED, PendingDeprecationWarning, stacklevel=2
1657
+ _DOWNLOAD_AS_STRING_DEPRECATED,
1658
+ PendingDeprecationWarning,
1659
+ stacklevel=2,
1647
1660
  )
1648
1661
  with create_trace_span(name="Storage.Blob.downloadAsString"):
1649
1662
  return self.download_as_bytes(
@@ -1999,12 +2012,18 @@ class Blob(_PropertyMixin):
1999
2012
  transport = self._get_transport(client)
2000
2013
  if "metadata" in self._properties and "metadata" not in self._changes:
2001
2014
  self._changes.add("metadata")
2015
+
2002
2016
  info = self._get_upload_arguments(client, content_type, command=command)
2003
2017
  headers, object_metadata, content_type = info
2004
2018
 
2019
+ if "crc32c" in self._properties:
2020
+ object_metadata["crc32c"] = self._properties["crc32c"]
2021
+
2005
2022
  hostname = _get_host_name(client._connection)
2006
2023
  base_url = _MULTIPART_URL_TEMPLATE.format(
2007
- hostname=hostname, bucket_path=self.bucket.path, api_version=_API_VERSION
2024
+ hostname=hostname,
2025
+ bucket_path=self.bucket.path,
2026
+ api_version=_API_VERSION,
2008
2027
  )
2009
2028
  name_value_pairs = []
2010
2029
 
@@ -2195,9 +2214,14 @@ class Blob(_PropertyMixin):
2195
2214
  if extra_headers is not None:
2196
2215
  headers.update(extra_headers)
2197
2216
 
2217
+ if "crc32c" in self._properties:
2218
+ object_metadata["crc32c"] = self._properties["crc32c"]
2219
+
2198
2220
  hostname = _get_host_name(client._connection)
2199
2221
  base_url = _RESUMABLE_URL_TEMPLATE.format(
2200
- hostname=hostname, bucket_path=self.bucket.path, api_version=_API_VERSION
2222
+ hostname=hostname,
2223
+ bucket_path=self.bucket.path,
2224
+ api_version=_API_VERSION,
2201
2225
  )
2202
2226
  name_value_pairs = []
2203
2227
 
@@ -2234,7 +2258,11 @@ class Blob(_PropertyMixin):
2234
2258
 
2235
2259
  upload_url = _add_query_parameters(base_url, name_value_pairs)
2236
2260
  upload = ResumableUpload(
2237
- upload_url, chunk_size, headers=headers, checksum=checksum, retry=retry
2261
+ upload_url,
2262
+ chunk_size,
2263
+ headers=headers,
2264
+ checksum=checksum,
2265
+ retry=retry,
2238
2266
  )
2239
2267
 
2240
2268
  upload.initiate(
@@ -3426,7 +3454,11 @@ class Blob(_PropertyMixin):
3426
3454
  return Policy.from_api_repr(info)
3427
3455
 
3428
3456
  def test_iam_permissions(
3429
- self, permissions, client=None, timeout=_DEFAULT_TIMEOUT, retry=DEFAULT_RETRY
3457
+ self,
3458
+ permissions,
3459
+ client=None,
3460
+ timeout=_DEFAULT_TIMEOUT,
3461
+ retry=DEFAULT_RETRY,
3430
3462
  ):
3431
3463
  """API call: test permissions
3432
3464
 
@@ -3693,7 +3725,10 @@ class Blob(_PropertyMixin):
3693
3725
 
3694
3726
  source_objects = []
3695
3727
  for source, source_generation in zip(sources, if_source_generation_match):
3696
- source_object = {"name": source.name, "generation": source.generation}
3728
+ source_object = {
3729
+ "name": source.name,
3730
+ "generation": source.generation,
3731
+ }
3697
3732
 
3698
3733
  preconditions = {}
3699
3734
  if source_generation is not None:
@@ -4154,7 +4189,10 @@ class Blob(_PropertyMixin):
4154
4189
  "encoding, errors and newline arguments are for text mode only"
4155
4190
  )
4156
4191
  return BlobWriter(
4157
- self, chunk_size=chunk_size, ignore_flush=ignore_flush, **kwargs
4192
+ self,
4193
+ chunk_size=chunk_size,
4194
+ ignore_flush=ignore_flush,
4195
+ **kwargs,
4158
4196
  )
4159
4197
  elif mode in ("r", "rt"):
4160
4198
  if ignore_flush:
@@ -386,7 +386,10 @@ class LifecycleRuleSetStorageClass(dict):
386
386
  def __init__(self, storage_class, **kw):
387
387
  conditions = LifecycleRuleConditions(**kw)
388
388
  rule = {
389
- "action": {"type": "SetStorageClass", "storageClass": storage_class},
389
+ "action": {
390
+ "type": "SetStorageClass",
391
+ "storageClass": storage_class,
392
+ },
390
393
  "condition": dict(conditions),
391
394
  }
392
395
  super().__init__(rule)
@@ -846,6 +849,7 @@ class Bucket(_PropertyMixin):
846
849
  encryption_key=None,
847
850
  kms_key_name=None,
848
851
  generation=None,
852
+ crc32c_checksum=None,
849
853
  ):
850
854
  """Factory constructor for blob object.
851
855
 
@@ -873,6 +877,14 @@ class Bucket(_PropertyMixin):
873
877
  :param generation: (Optional) If present, selects a specific revision of
874
878
  this object.
875
879
 
880
+ :type crc32c_checksum: str
881
+ :param crc32c_checksum:
882
+ (Optional) If set, the CRC32C checksum of the blob's content.
883
+ CRC32c checksum, as described in RFC 4960, Appendix B; encoded using
884
+ base64 in big-endian byte order. See
885
+ Apenndix B: https://datatracker.ietf.org/doc/html/rfc4960#appendix-B
886
+ base64: https://datatracker.ietf.org/doc/html/rfc4648#section-4
887
+
876
888
  :rtype: :class:`google.cloud.storage.blob.Blob`
877
889
  :returns: The blob object created.
878
890
  """
@@ -883,6 +895,7 @@ class Bucket(_PropertyMixin):
883
895
  encryption_key=encryption_key,
884
896
  kms_key_name=kms_key_name,
885
897
  generation=generation,
898
+ crc32c_checksum=crc32c_checksum,
886
899
  )
887
900
 
888
901
  def notification(
@@ -3253,7 +3266,10 @@ class Bucket(_PropertyMixin):
3253
3266
  :type not_found_page: str
3254
3267
  :param not_found_page: The file to use when a page isn't found.
3255
3268
  """
3256
- data = {"mainPageSuffix": main_page_suffix, "notFoundPage": not_found_page}
3269
+ data = {
3270
+ "mainPageSuffix": main_page_suffix,
3271
+ "notFoundPage": not_found_page,
3272
+ }
3257
3273
  self._patch_property("website", data)
3258
3274
 
3259
3275
  def disable_website(self):
@@ -3385,7 +3401,11 @@ class Bucket(_PropertyMixin):
3385
3401
  return Policy.from_api_repr(info)
3386
3402
 
3387
3403
  def test_iam_permissions(
3388
- self, permissions, client=None, timeout=_DEFAULT_TIMEOUT, retry=DEFAULT_RETRY
3404
+ self,
3405
+ permissions,
3406
+ client=None,
3407
+ timeout=_DEFAULT_TIMEOUT,
3408
+ retry=DEFAULT_RETRY,
3389
3409
  ):
3390
3410
  """API call: test permissions
3391
3411
 
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "3.3.0"
15
+ __version__ = "3.3.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-cloud-storage
3
- Version: 3.3.0
3
+ Version: 3.3.1
4
4
  Summary: Google Cloud Storage API client library
5
5
  Home-page: https://github.com/googleapis/python-storage
6
6
  Author: Google LLC
@@ -61,6 +61,7 @@ tests/conformance/retry_strategy_test_data.json
61
61
  tests/conformance/test_conformance.py
62
62
  tests/data/CloudPlatform_128px_Retina.png
63
63
  tests/data/five-point-one-mb-file.zip
64
+ tests/data/random_9_MiB_file
64
65
  tests/data/simple.txt
65
66
  tests/perf/README.md
66
67
  tests/perf/_perf_utils.py
@@ -42,6 +42,11 @@ EXAMPLE_XML_MPU_INITIATE_TEXT_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"
42
42
  UPLOAD_ID = "VXBsb2FkIElEIGZvciBlbHZpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
43
43
  PARTS = {1: "39a59594290b0f9a30662a56d695b71d", 2: "00000000290b0f9a30662a56d695b71d"}
44
44
  FILE_DATA = b"testdata" * 128
45
+ _HASH_HEADER = "x-goog-hash"
46
+ CRC32C_HASH_OF_FIRST_PART = "8hVqVQ=="
47
+ MD5_HASH_OF_FIRST_PART = "gfVZ4+0LdooJwGAkxLrCcg=="
48
+ DEFAULT_CONNECT_TIMEOUT = 61
49
+ DEFAULT_READ_TIMEOUT = 60
45
50
 
46
51
 
47
52
  @pytest.fixture(scope="session")
@@ -402,6 +407,66 @@ def test_mpu_part(filename):
402
407
  assert part.etag == PARTS[1]
403
408
 
404
409
 
410
+ def test_mpu_part_with_md5_enabled(filename):
411
+ part = upload_mod.XMLMPUPart(
412
+ EXAMPLE_XML_UPLOAD_URL,
413
+ UPLOAD_ID,
414
+ filename,
415
+ start=0,
416
+ end=128,
417
+ part_number=1,
418
+ checksum="md5",
419
+ )
420
+
421
+ transport = mock.Mock(spec=["request"])
422
+ transport.request.return_value = _make_response(
423
+ headers={"etag": PARTS[1], _HASH_HEADER: f"md5={MD5_HASH_OF_FIRST_PART}"}
424
+ )
425
+
426
+ part.upload(transport)
427
+
428
+ transport.request.assert_called_once_with(
429
+ "PUT",
430
+ f"{part.upload_url}?partNumber={part.part_number}&uploadId={UPLOAD_ID}",
431
+ data=FILE_DATA[part.start : part.end],
432
+ headers={"X-Goog-Hash": f"md5={MD5_HASH_OF_FIRST_PART}"},
433
+ timeout=(DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT),
434
+ )
435
+
436
+ assert part.finished
437
+ assert part.etag == PARTS[1]
438
+
439
+
440
+ def test_mpu_part_with_crc32c_enabled(filename):
441
+ part = upload_mod.XMLMPUPart(
442
+ EXAMPLE_XML_UPLOAD_URL,
443
+ UPLOAD_ID,
444
+ filename,
445
+ start=0,
446
+ end=128,
447
+ part_number=1,
448
+ checksum="crc32c",
449
+ )
450
+
451
+ transport = mock.Mock(spec=["request"])
452
+ transport.request.return_value = _make_response(
453
+ headers={"etag": PARTS[1], _HASH_HEADER: f"crc32c={CRC32C_HASH_OF_FIRST_PART}"}
454
+ )
455
+
456
+ part.upload(transport)
457
+
458
+ transport.request.assert_called_once_with(
459
+ "PUT",
460
+ f"{part.upload_url}?partNumber={part.part_number}&uploadId={UPLOAD_ID}",
461
+ data=FILE_DATA[part.start : part.end],
462
+ headers={"X-Goog-Hash": f"crc32c={CRC32C_HASH_OF_FIRST_PART}"},
463
+ timeout=(DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT),
464
+ )
465
+
466
+ assert part.finished
467
+ assert part.etag == PARTS[1]
468
+
469
+
405
470
  def _make_response(status_code=http.client.OK, headers=None, text=None):
406
471
  headers = headers or {}
407
472
  return mock.Mock(
@@ -47,6 +47,21 @@ EXAMPLE_XML_MPU_INITIATE_TEXT_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"
47
47
  UPLOAD_ID = "VXBsb2FkIElEIGZvciBlbHZpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
48
48
  PARTS = {1: "39a59594290b0f9a30662a56d695b71d", 2: "00000000290b0f9a30662a56d695b71d"}
49
49
  FILE_DATA = b"testdata" * 128
50
+ CHECKSUM_MISMATCH_ERROR_MSG_XML_TEMPLATE = """<?xml version='1.0' encoding='UTF-8'?>
51
+ <Error>
52
+ <Code>{ERROR_CODE}</Code>
53
+ <Message>The MD5 you specified in Content-MD5 or x-goog-hash was invalid.</Message>
54
+ <Details>Invalid MD5 value: dfdfdfd==</Details>
55
+ </Error>"""
56
+ INVALID_MD5_XML_RESPONSE = CHECKSUM_MISMATCH_ERROR_MSG_XML_TEMPLATE.format(
57
+ ERROR_CODE="InvalidDigest"
58
+ )
59
+ INVALID_CRC32C_XML_RESPONSE = CHECKSUM_MISMATCH_ERROR_MSG_XML_TEMPLATE.format(
60
+ ERROR_CODE="BadDigest"
61
+ )
62
+ INCORRECT_LENGTH_CRC32C_XML_RESPONSE = CHECKSUM_MISMATCH_ERROR_MSG_XML_TEMPLATE.format(
63
+ ERROR_CODE="CrcMismatch"
64
+ )
50
65
 
51
66
 
52
67
  @pytest.fixture(scope="session")
@@ -1471,7 +1486,15 @@ def test_xml_mpu_part_invalid_response(filename):
1471
1486
  part._process_upload_response(response)
1472
1487
 
1473
1488
 
1474
- def test_xml_mpu_part_checksum_failure(filename):
1489
+ @pytest.mark.parametrize(
1490
+ "error_scenarios",
1491
+ [
1492
+ INVALID_MD5_XML_RESPONSE,
1493
+ INVALID_CRC32C_XML_RESPONSE,
1494
+ INCORRECT_LENGTH_CRC32C_XML_RESPONSE,
1495
+ ],
1496
+ )
1497
+ def test_xml_mpu_part_checksum_failure(filename, error_scenarios):
1475
1498
  PART_NUMBER = 1
1476
1499
  START = 0
1477
1500
  END = 256
@@ -1490,7 +1513,9 @@ def test_xml_mpu_part_checksum_failure(filename):
1490
1513
  _fix_up_virtual(part)
1491
1514
  part._prepare_upload_request()
1492
1515
  response = _make_xml_response(
1493
- headers={"etag": ETAG, "x-goog-hash": "md5=Ojk9c3dhfxgoKVVHYwFbHQ=="}
1516
+ status_code=http.client.BAD_REQUEST,
1517
+ headers={"etag": ETAG, "x-goog-hash": "md5=Ojk9c3dhfxgoKVVHYwFbHQ=="},
1518
+ text=error_scenarios,
1494
1519
  ) # Example md5 checksum but not the correct one
1495
1520
  with pytest.raises(DataCorruption):
1496
1521
  part._process_upload_response(response)
@@ -1555,7 +1580,7 @@ def _make_xml_response(status_code=http.client.OK, headers=None, text=None):
1555
1580
  headers=headers,
1556
1581
  status_code=status_code,
1557
1582
  text=text,
1558
- spec=["headers", "status_code"],
1583
+ spec=["headers", "status_code", "text"],
1559
1584
  )
1560
1585
 
1561
1586
 
@@ -30,6 +30,7 @@ _filenames = [
30
30
  ("logo", "CloudPlatform_128px_Retina.png"),
31
31
  ("big", "five-point-one-mb-file.zip"),
32
32
  ("simple", "simple.txt"),
33
+ ("big_9MiB", "random_9_MiB_file"),
33
34
  ]
34
35
  _file_data = {
35
36
  key: {"path": os.path.join(data_dirname, file_name)}
@@ -59,9 +59,7 @@ def _create_signed_list_blobs_url_helper(
59
59
  :Response headers end."
60
60
 
61
61
 
62
- def test_create_signed_list_blobs_url_v2(
63
- storage_client, signing_bucket, no_mtls
64
- ):
62
+ def test_create_signed_list_blobs_url_v2(storage_client, signing_bucket, no_mtls):
65
63
  _create_signed_list_blobs_url_helper(
66
64
  storage_client,
67
65
  signing_bucket,
@@ -83,9 +81,7 @@ def test_create_signed_list_blobs_url_v2_w_expiration(
83
81
  )
84
82
 
85
83
 
86
- def test_create_signed_list_blobs_url_v4(
87
- storage_client, signing_bucket, no_mtls
88
- ):
84
+ def test_create_signed_list_blobs_url_v4(storage_client, signing_bucket, no_mtls):
89
85
  _create_signed_list_blobs_url_helper(
90
86
  storage_client,
91
87
  signing_bucket,
@@ -229,9 +225,7 @@ def test_create_signed_read_url_v4_w_non_ascii_name(
229
225
  )
230
226
 
231
227
 
232
- def test_create_signed_read_url_v2_w_csek(
233
- storage_client, signing_bucket, no_mtls
234
- ):
228
+ def test_create_signed_read_url_v2_w_csek(storage_client, signing_bucket, no_mtls):
235
229
  encryption_key = os.urandom(32)
236
230
  _create_signed_read_url_helper(
237
231
  storage_client,
@@ -242,9 +236,7 @@ def test_create_signed_read_url_v2_w_csek(
242
236
  )
243
237
 
244
238
 
245
- def test_create_signed_read_url_v4_w_csek(
246
- storage_client, signing_bucket, no_mtls
247
- ):
239
+ def test_create_signed_read_url_v4_w_csek(storage_client, signing_bucket, no_mtls):
248
240
  encryption_key = os.urandom(32)
249
241
  _create_signed_read_url_helper(
250
242
  storage_client,
@@ -327,9 +319,7 @@ def test_create_signed_read_url_v4_w_access_token_universe_domain(
327
319
  "https://www.googleapis.com/auth/devstorage.read_write",
328
320
  "https://www.googleapis.com/auth/iam",
329
321
  ]
330
- response = universe_domain_iam_client.generate_access_token(
331
- name=name, scope=scope
332
- )
322
+ response = universe_domain_iam_client.generate_access_token(name=name, scope=scope)
333
323
 
334
324
  _create_signed_read_url_helper(
335
325
  universe_domain_client,
@@ -340,9 +330,7 @@ def test_create_signed_read_url_v4_w_access_token_universe_domain(
340
330
  )
341
331
 
342
332
 
343
- def _create_signed_delete_url_helper(
344
- client, bucket, version="v2", expiration=None
345
- ):
333
+ def _create_signed_delete_url_helper(client, bucket, version="v2", expiration=None):
346
334
  expiration = _morph_expiration(version, expiration)
347
335
 
348
336
  blob = bucket.blob("DELETE_ME.txt")
@@ -372,9 +360,7 @@ def test_create_signed_delete_url_v2(storage_client, signing_bucket, no_mtls):
372
360
 
373
361
 
374
362
  def test_create_signed_delete_url_v4(storage_client, signing_bucket, no_mtls):
375
- _create_signed_delete_url_helper(
376
- storage_client, signing_bucket, version="v4"
377
- )
363
+ _create_signed_delete_url_helper(storage_client, signing_bucket, version="v4")
378
364
 
379
365
 
380
366
  def _create_signed_resumable_upload_url_helper(
@@ -393,9 +379,7 @@ def _create_signed_resumable_upload_url_helper(
393
379
  )
394
380
 
395
381
  post_headers = {"x-goog-resumable": "start"}
396
- post_response = requests.post(
397
- signed_resumable_upload_url, headers=post_headers
398
- )
382
+ post_response = requests.post(signed_resumable_upload_url, headers=post_headers)
399
383
  assert (
400
384
  post_response.status_code == 201
401
385
  ), f"Response content start: {post_response.content} \
@@ -445,9 +429,7 @@ def _create_signed_resumable_upload_url_helper(
445
429
  :Response headers end."
446
430
 
447
431
 
448
- def test_create_signed_resumable_upload_url_v2(
449
- storage_client, signing_bucket, no_mtls
450
- ):
432
+ def test_create_signed_resumable_upload_url_v2(storage_client, signing_bucket, no_mtls):
451
433
  _create_signed_resumable_upload_url_helper(
452
434
  storage_client,
453
435
  signing_bucket,
@@ -455,9 +437,7 @@ def test_create_signed_resumable_upload_url_v2(
455
437
  )
456
438
 
457
439
 
458
- def test_create_signed_resumable_upload_url_v4(
459
- storage_client, signing_bucket, no_mtls
460
- ):
440
+ def test_create_signed_resumable_upload_url_v4(storage_client, signing_bucket, no_mtls):
461
441
  _create_signed_resumable_upload_url_helper(
462
442
  storage_client,
463
443
  signing_bucket,
@@ -494,9 +474,7 @@ def test_generate_signed_post_policy_v4(
494
474
  )
495
475
  with open(blob_name, "r") as f:
496
476
  files = {"file": (blob_name, f)}
497
- response = requests.post(
498
- policy["url"], data=policy["fields"], files=files
499
- )
477
+ response = requests.post(policy["url"], data=policy["fields"], files=files)
500
478
 
501
479
  os.remove(blob_name)
502
480
  assert (
@@ -549,9 +527,7 @@ def test_generate_signed_post_policy_v4_access_token_sa_email(
549
527
  )
550
528
  with open(blob_name, "r") as f:
551
529
  files = {"file": (blob_name, f)}
552
- response = requests.post(
553
- policy["url"], data=policy["fields"], files=files
554
- )
530
+ response = requests.post(policy["url"], data=policy["fields"], files=files)
555
531
 
556
532
  os.remove(blob_name)
557
533
  assert (
@@ -591,9 +567,7 @@ def test_generate_signed_post_policy_v4_invalid_field(
591
567
  )
592
568
  with open(blob_name, "r") as f:
593
569
  files = {"file": (blob_name, f)}
594
- response = requests.post(
595
- policy["url"], data=policy["fields"], files=files
596
- )
570
+ response = requests.post(policy["url"], data=policy["fields"], files=files)
597
571
 
598
572
  os.remove(blob_name)
599
573
  assert (