diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b9303c8..9fea289e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ [1]: https://pypi.org/project/google-cloud-storage/#history +### [1.31.1](https://www.github.com/googleapis/python-storage/compare/v1.31.0...v1.31.1) (2020-09-16) + + +### Bug Fixes + +* add requests as a dependency ([#271](https://www.github.com/googleapis/python-storage/issues/271)) ([ec52b38](https://www.github.com/googleapis/python-storage/commit/ec52b38df211fad18a86d7e16d83db79de59d5f5)) +* preserve existing blob hashes when 'X-Goog-Hash header' is not present ([#267](https://www.github.com/googleapis/python-storage/issues/267)) ([277afb8](https://www.github.com/googleapis/python-storage/commit/277afb83f464d77b163f2722272092df4180411e)) +* **blob:** base64 includes additional characters ([#258](https://www.github.com/googleapis/python-storage/issues/258)) ([cf0774a](https://www.github.com/googleapis/python-storage/commit/cf0774aa8ffd45d340aff9a7d2236d8d65c8ae93)) + + +### Documentation + +* add docs signed_url expiration take default utc ([#250](https://www.github.com/googleapis/python-storage/issues/250)) ([944ab18](https://www.github.com/googleapis/python-storage/commit/944ab1827b3ca0bd1d3aafc2829245290e9bde59)) + ## [1.31.0](https://www.github.com/googleapis/python-storage/compare/v1.30.0...v1.31.0) (2020-08-26) diff --git a/google/cloud/storage/_signing.py b/google/cloud/storage/_signing.py index 8e18bdaea..1382ebc77 100644 --- a/google/cloud/storage/_signing.py +++ b/google/cloud/storage/_signing.py @@ -91,7 +91,9 @@ def get_expiration_seconds_v2(expiration): """Convert 'expiration' to a number of seconds in the future. :type expiration: Union[Integer, datetime.datetime, datetime.timedelta] - :param expiration: Point in time when the signed URL should expire. + :param expiration: Point in time when the signed URL should expire. If + a ``datetime`` instance is passed without an explicit + ``tzinfo`` set, it will be assumed to be ``UTC``. :raises: :exc:`TypeError` when expiration is not a valid type. @@ -123,7 +125,9 @@ def get_expiration_seconds_v4(expiration): """Convert 'expiration' to a number of seconds offset from the current time. :type expiration: Union[Integer, datetime.datetime, datetime.timedelta] - :param expiration: Point in time when the signed URL should expire. + :param expiration: Point in time when the signed URL should expire. If + a ``datetime`` instance is passed without an explicit + ``tzinfo`` set, it will be assumed to be ``UTC``. :raises: :exc:`TypeError` when expiration is not a valid type. :raises: :exc:`ValueError` when expiration is too large. @@ -299,7 +303,9 @@ def generate_signed_url_v2( Caller should have already URL-encoded the value. :type expiration: Union[Integer, datetime.datetime, datetime.timedelta] - :param expiration: Point in time when the signed URL should expire. + :param expiration: Point in time when the signed URL should expire. If + a ``datetime`` instance is passed without an explicit + ``tzinfo`` set, it will be assumed to be ``UTC``. :type api_access_endpoint: str :param api_access_endpoint: (Optional) URI base. Defaults to empty string. @@ -461,7 +467,9 @@ def generate_signed_url_v4( Caller should have already URL-encoded the value. :type expiration: Union[Integer, datetime.datetime, datetime.timedelta] - :param expiration: Point in time when the signed URL should expire. + :param expiration: Point in time when the signed URL should expire. If + a ``datetime`` instance is passed without an explicit + ``tzinfo`` set, it will be assumed to be ``UTC``. :type api_access_endpoint: str :param api_access_endpoint: (Optional) URI base. Defaults to diff --git a/google/cloud/storage/blob.py b/google/cloud/storage/blob.py index 28b16682d..a7e6952bf 100644 --- a/google/cloud/storage/blob.py +++ b/google/cloud/storage/blob.py @@ -418,7 +418,9 @@ def generate_signed_url( log in. :type expiration: Union[Integer, datetime.datetime, datetime.timedelta] - :param expiration: Point in time when the signed URL should expire. + :param expiration: Point in time when the signed URL should expire. If + a ``datetime`` instance is passed without an explicit + ``tzinfo`` set, it will be assumed to be ``UTC``. :type api_access_endpoint: str :param api_access_endpoint: (Optional) URI base. @@ -809,15 +811,16 @@ def _extract_headers_from_download(self, response): # 'X-Goog-Hash': 'crc32c=4gcgLQ==,md5=CS9tHYTtyFntzj7B9nkkJQ==', x_goog_hash = response.headers.get("X-Goog-Hash", "") - digests = {} - for encoded_digest in x_goog_hash.split(","): - match = re.match(r"(crc32c|md5)=([\w\d/]+={0,3})", encoded_digest) - if match: - method, digest = match.groups() - digests[method] = digest + if x_goog_hash: + digests = {} + for encoded_digest in x_goog_hash.split(","): + match = re.match(r"(crc32c|md5)=([\w\d/\+/]+={0,3})", encoded_digest) + if match: + method, digest = match.groups() + digests[method] = digest - self.crc32c = digests.get("crc32c", None) - self.md5_hash = digests.get("md5", None) + self.crc32c = digests.get("crc32c", None) + self.md5_hash = digests.get("md5", None) def _do_download( self, diff --git a/google/cloud/storage/bucket.py b/google/cloud/storage/bucket.py index e68703fac..adf37d398 100644 --- a/google/cloud/storage/bucket.py +++ b/google/cloud/storage/bucket.py @@ -3020,7 +3020,9 @@ def generate_signed_url( log in. :type expiration: Union[Integer, datetime.datetime, datetime.timedelta] - :param expiration: Point in time when the signed URL should expire. + :param expiration: Point in time when the signed URL should expire. If + a ``datetime`` instance is passed without an explicit + ``tzinfo`` set, it will be assumed to be ``UTC``. :type api_access_endpoint: str :param api_access_endpoint: (Optional) URI base. diff --git a/google/cloud/storage/client.py b/google/cloud/storage/client.py index 6c7fa73c8..fd29abe9c 100644 --- a/google/cloud/storage/client.py +++ b/google/cloud/storage/client.py @@ -962,7 +962,9 @@ def generate_signed_post_policy_v4( :param blob_name: Object name. :type expiration: Union[Integer, datetime.datetime, datetime.timedelta] - :param expiration: Policy expiration time. + :param expiration: Policy expiration time. If a ``datetime`` instance is + passed without an explicit ``tzinfo`` set, it will be + assumed to be ``UTC``. :type conditions: list :param conditions: (Optional) List of POST policy conditions, which are @@ -1004,11 +1006,13 @@ def generate_signed_post_policy_v4( Generate signed POST policy and upload a file. >>> from google.cloud import storage + >>> import pytz >>> client = storage.Client() + >>> tz = pytz.timezone('America/New_York') >>> policy = client.generate_signed_post_policy_v4( "bucket-name", "blob-name", - expiration=datetime.datetime(2020, 3, 17), + expiration=datetime.datetime(2020, 3, 17, tzinfo=tz), conditions=[ ["content-length-range", 0, 255] ], diff --git a/noxfile.py b/noxfile.py index 694b5e627..42e5ff31e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -124,7 +124,7 @@ def system(session): "pytest", "google-cloud-testutils", "google-cloud-iam", - "google-cloud-pubsub", + "google-cloud-pubsub < 2.0.0", "google-cloud-kms < 2.0dev", ) session.install("-e", ".") diff --git a/setup.py b/setup.py index fd678eee0..3393c1fe8 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ name = "google-cloud-storage" description = "Google Cloud Storage API client library" -version = "1.31.0" +version = "1.31.1" # Should be one of: # 'Development Status :: 3 - Alpha' # 'Development Status :: 4 - Beta' @@ -32,6 +32,7 @@ "google-auth >= 1.11.0, < 2.0dev", "google-cloud-core >= 1.4.1, < 2.0dev", "google-resumable-media >= 1.0.0, < 2.0dev", + "requests >= 2.18.0, < 3.0.0dev", ] extras = {} diff --git a/synth.metadata b/synth.metadata index e92268768..09b265914 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,15 +4,78 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/python-storage.git", - "sha": "e1f91fcca6c001bc3b0c5f759a7a003fcf60c0a6" + "sha": "944ab1827b3ca0bd1d3aafc2829245290e9bde59" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "4f8f5dc24af79694887385015294e4dbb214c352" + "sha": "80f46100c047bc47efe0025ee537dc8ee413ad04" } } + ], + "generatedFiles": [ + ".coveragerc", + ".flake8", + ".github/CONTRIBUTING.md", + ".github/ISSUE_TEMPLATE/bug_report.md", + ".github/ISSUE_TEMPLATE/feature_request.md", + ".github/ISSUE_TEMPLATE/support_request.md", + ".github/PULL_REQUEST_TEMPLATE.md", + ".github/release-please.yml", + ".gitignore", + ".kokoro/build.sh", + ".kokoro/continuous/common.cfg", + ".kokoro/continuous/continuous.cfg", + ".kokoro/docker/docs/Dockerfile", + ".kokoro/docker/docs/fetch_gpg_keys.sh", + ".kokoro/docs/common.cfg", + ".kokoro/docs/docs-presubmit.cfg", + ".kokoro/docs/docs.cfg", + ".kokoro/presubmit/common.cfg", + ".kokoro/presubmit/presubmit.cfg", + ".kokoro/publish-docs.sh", + ".kokoro/release.sh", + ".kokoro/release/common.cfg", + ".kokoro/release/release.cfg", + ".kokoro/samples/lint/common.cfg", + ".kokoro/samples/lint/continuous.cfg", + ".kokoro/samples/lint/periodic.cfg", + ".kokoro/samples/lint/presubmit.cfg", + ".kokoro/samples/python3.6/common.cfg", + ".kokoro/samples/python3.6/continuous.cfg", + ".kokoro/samples/python3.6/periodic.cfg", + ".kokoro/samples/python3.6/presubmit.cfg", + ".kokoro/samples/python3.7/common.cfg", + ".kokoro/samples/python3.7/continuous.cfg", + ".kokoro/samples/python3.7/periodic.cfg", + ".kokoro/samples/python3.7/presubmit.cfg", + ".kokoro/samples/python3.8/common.cfg", + ".kokoro/samples/python3.8/continuous.cfg", + ".kokoro/samples/python3.8/periodic.cfg", + ".kokoro/samples/python3.8/presubmit.cfg", + ".kokoro/test-samples.sh", + ".kokoro/trampoline.sh", + ".kokoro/trampoline_v2.sh", + ".trampolinerc", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.rst", + "LICENSE", + "MANIFEST.in", + "docs/_static/custom.css", + "docs/_templates/layout.html", + "docs/conf.py", + "noxfile.py", + "renovate.json", + "scripts/decrypt-secrets.sh", + "scripts/readme-gen/readme_gen.py", + "scripts/readme-gen/templates/README.tmpl.rst", + "scripts/readme-gen/templates/auth.tmpl.rst", + "scripts/readme-gen/templates/auth_api_key.tmpl.rst", + "scripts/readme-gen/templates/install_deps.tmpl.rst", + "scripts/readme-gen/templates/install_portaudio.tmpl.rst", + "setup.cfg", + "testing/.gitignore" ] } \ No newline at end of file diff --git a/synth.py b/synth.py index 577248cba..7d616d44d 100644 --- a/synth.py +++ b/synth.py @@ -28,7 +28,7 @@ cov_level=99, system_test_external_dependencies=[ "google-cloud-iam", - "google-cloud-pubsub", + "google-cloud-pubsub < 2.0.0", # See: https://github.com/googleapis/python-storage/issues/226 "google-cloud-kms < 2.0dev", ], diff --git a/tests/unit/test_blob.py b/tests/unit/test_blob.py index 9bf60d42d..f67b6501e 100644 --- a/tests/unit/test_blob.py +++ b/tests/unit/test_blob.py @@ -1522,12 +1522,36 @@ def test_download_as_string_w_response_headers(self): self.assertEqual(blob.md5_hash, "CS9tHYTtyFntzj7B9nkkJQ==") self.assertEqual(blob.crc32c, "4gcgLQ==") + response = self._mock_requests_response( + http_client.OK, + headers={ + "Content-Type": "application/octet-stream", + "Content-Language": "en-US", + "Cache-Control": "max-age=1337;public", + "Content-Encoding": "gzip", + "X-Goog-Storage-Class": "STANDARD", + "X-Goog-Hash": "crc32c=4/c+LQ==,md5=CS9tHYTt/+ntzj7B9nkkJQ==", + }, + content=b"", + ) + blob._extract_headers_from_download(response) + self.assertEqual(blob.content_type, "application/octet-stream") + self.assertEqual(blob.content_language, "en-US") + self.assertEqual(blob.md5_hash, "CS9tHYTt/+ntzj7B9nkkJQ==") + self.assertEqual(blob.crc32c, "4/c+LQ==") + def test_download_as_string_w_hash_response_header_none(self): blob_name = "blob-name" + md5_hash = "CS9tHYTtyFntzj7B9nkkJQ==" + crc32c = "4gcgLQ==" client = mock.Mock(spec=["_http"]) bucket = _Bucket(client) media_link = "http://example.com/media/" - properties = {"mediaLink": media_link} + properties = { + "mediaLink": media_link, + "md5Hash": md5_hash, + "crc32c": crc32c, + } blob = self._make_one(blob_name, bucket=bucket, properties=properties) response = self._mock_requests_response( @@ -1538,8 +1562,8 @@ def test_download_as_string_w_hash_response_header_none(self): ) blob._extract_headers_from_download(response) - self.assertIsNone(blob.md5_hash) - self.assertIsNone(blob.crc32c) + self.assertEqual(blob.md5_hash, md5_hash) + self.assertEqual(blob.crc32c, crc32c) def test_download_as_bytes_w_generation_match(self): GENERATION_NUMBER = 6