Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c7bf615

Browse files
author
Aaron Gabriel Neyer
authored
fix: add user agent in python-storage when calling resumable media (WIP) (googleapis#715)
* fix: add user agent in python-storage when calling resumable media (WIP) * put the things in the right places * starting to get some tests working * a bit closer, still some things not quite working * almost there * first cleanup * ensure up to date resumable media * next round of cleanup * lint * update resumable media * get tests passing * lint
1 parent 4fbbf02 commit c7bf615

File tree

6 files changed

+107
-34
lines changed

6 files changed

+107
-34
lines changed

google/cloud/storage/_helpers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,3 +581,25 @@ def _api_core_retry_to_resumable_media_retry(retry, num_retries=None):
581581
return resumable_media.RetryStrategy(max_retries=num_retries)
582582
else:
583583
return resumable_media.RetryStrategy(max_retries=0)
584+
585+
586+
def _get_default_headers(
587+
user_agent,
588+
content_type="application/json; charset=UTF-8",
589+
x_upload_content_type=None,
590+
):
591+
"""Get the headers for a request.
592+
593+
Args:
594+
user_agent (str): The user-agent for requests.
595+
Returns:
596+
Dict: The headers to be used for the request.
597+
"""
598+
return {
599+
"Accept": "application/json",
600+
"Accept-Encoding": "gzip, deflate",
601+
"User-Agent": user_agent,
602+
"x-goog-api-client": user_agent,
603+
"content-type": content_type,
604+
"x-upload-content-type": x_upload_content_type or content_type,
605+
}

google/cloud/storage/blob.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
from google.cloud.storage._helpers import _bucket_bound_hostname_url
6565
from google.cloud.storage._helpers import _raise_if_more_than_one_set
6666
from google.cloud.storage._helpers import _api_core_retry_to_resumable_media_retry
67+
from google.cloud.storage._helpers import _get_default_headers
6768
from google.cloud.storage._signing import generate_signed_url_v2
6869
from google.cloud.storage._signing import generate_signed_url_v4
6970
from google.cloud.storage._helpers import _NUM_RETRIES_MESSAGE
@@ -1720,7 +1721,7 @@ def _get_writable_metadata(self):
17201721

17211722
return object_metadata
17221723

1723-
def _get_upload_arguments(self, content_type):
1724+
def _get_upload_arguments(self, client, content_type):
17241725
"""Get required arguments for performing an upload.
17251726
17261727
The content type returned will be determined in order of precedence:
@@ -1739,9 +1740,12 @@ def _get_upload_arguments(self, content_type):
17391740
* An object metadata dictionary
17401741
* The ``content_type`` as a string (according to precedence)
17411742
"""
1742-
headers = _get_encryption_headers(self._encryption_key)
1743-
object_metadata = self._get_writable_metadata()
17441743
content_type = self._get_content_type(content_type)
1744+
headers = {
1745+
**_get_default_headers(client._connection.user_agent, content_type),
1746+
**_get_encryption_headers(self._encryption_key),
1747+
}
1748+
object_metadata = self._get_writable_metadata()
17451749
return headers, object_metadata, content_type
17461750

17471751
def _do_multipart_upload(
@@ -1860,7 +1864,7 @@ def _do_multipart_upload(
18601864
transport = self._get_transport(client)
18611865
if "metadata" in self._properties and "metadata" not in self._changes:
18621866
self._changes.add("metadata")
1863-
info = self._get_upload_arguments(content_type)
1867+
info = self._get_upload_arguments(client, content_type)
18641868
headers, object_metadata, content_type = info
18651869

18661870
hostname = _get_host_name(client._connection)
@@ -2045,7 +2049,7 @@ def _initiate_resumable_upload(
20452049
transport = self._get_transport(client)
20462050
if "metadata" in self._properties and "metadata" not in self._changes:
20472051
self._changes.add("metadata")
2048-
info = self._get_upload_arguments(content_type)
2052+
info = self._get_upload_arguments(client, content_type)
20492053
headers, object_metadata, content_type = info
20502054
if extra_headers is not None:
20512055
headers.update(extra_headers)
@@ -2230,15 +2234,13 @@ def _do_resumable_upload(
22302234
checksum=checksum,
22312235
retry=retry,
22322236
)
2233-
22342237
while not upload.finished:
22352238
try:
22362239
response = upload.transmit_next_chunk(transport, timeout=timeout)
22372240
except resumable_media.DataCorruption:
22382241
# Attempt to delete the corrupted object.
22392242
self.delete()
22402243
raise
2241-
22422244
return response
22432245

22442246
def _do_upload(

google/cloud/storage/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from google.cloud._helpers import _LocalStack, _NOW
3232
from google.cloud.client import ClientWithProject
3333
from google.cloud.exceptions import NotFound
34+
from google.cloud.storage._helpers import _get_default_headers
3435
from google.cloud.storage._helpers import _get_environ_project
3536
from google.cloud.storage._helpers import _get_storage_host
3637
from google.cloud.storage._helpers import _BASE_STORAGE_URI
@@ -1131,6 +1132,7 @@ def download_blob_to_file(
11311132
_add_etag_match_headers(
11321133
headers, if_etag_match=if_etag_match, if_etag_not_match=if_etag_not_match,
11331134
)
1135+
headers = {**_get_default_headers(self._connection.user_agent), **headers}
11341136

11351137
transport = self._http
11361138
try:

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"google-auth >= 1.25.0, < 3.0dev",
3232
"google-api-core >= 1.31.5, <3.0.0dev,!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0",
3333
"google-cloud-core >= 1.6.0, < 3.0dev",
34-
"google-resumable-media >= 1.3.0",
34+
"google-resumable-media >= 2.3.2",
3535
"requests >= 2.18.0, < 3.0.0dev",
3636
"protobuf",
3737
]

tests/unit/test_blob.py

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import mock
2727
import pytest
2828

29+
from google.cloud.storage._helpers import _get_default_headers
2930
from google.cloud.storage.retry import (
3031
DEFAULT_RETRY,
3132
DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED,
@@ -2212,16 +2213,19 @@ def test__set_metadata_to_none(self):
22122213
def test__get_upload_arguments(self):
22132214
name = u"blob-name"
22142215
key = b"[pXw@,p@@AfBfrR3x-2b2SCHR,.?YwRO"
2216+
client = mock.Mock(_connection=_Connection)
2217+
client._connection.user_agent = "testing 1.2.3"
22152218
blob = self._make_one(name, bucket=None, encryption_key=key)
22162219
blob.content_disposition = "inline"
22172220

22182221
content_type = u"image/jpeg"
2219-
info = blob._get_upload_arguments(content_type)
2222+
info = blob._get_upload_arguments(client, content_type)
22202223

22212224
headers, object_metadata, new_content_type = info
22222225
header_key_value = "W3BYd0AscEBAQWZCZnJSM3gtMmIyU0NIUiwuP1l3Uk8="
22232226
header_key_hash_value = "G0++dxF4q5rG4o9kE8gvEKn15RH6wLm0wXV1MgAlXOg="
22242227
expected_headers = {
2228+
**_get_default_headers(client._connection.user_agent, content_type),
22252229
"X-Goog-Encryption-Algorithm": "AES256",
22262230
"X-Goog-Encryption-Key": header_key_value,
22272231
"X-Goog-Encryption-Key-Sha256": header_key_hash_value,
@@ -2368,7 +2372,11 @@ def _do_multipart_success(
23682372
+ data_read
23692373
+ b"\r\n--==0==--"
23702374
)
2371-
headers = {"content-type": b'multipart/related; boundary="==0=="'}
2375+
headers = _get_default_headers(
2376+
client._connection.user_agent,
2377+
b'multipart/related; boundary="==0=="',
2378+
"application/xml",
2379+
)
23722380
client._http.request.assert_called_once_with(
23732381
"POST", upload_url, data=payload, headers=headers, timeout=expected_timeout
23742382
)
@@ -2614,10 +2622,17 @@ def _initiate_resumable_helper(
26142622

26152623
self.assertEqual(upload.upload_url, upload_url)
26162624
if extra_headers is None:
2617-
self.assertEqual(upload._headers, {})
2625+
self.assertEqual(
2626+
upload._headers,
2627+
_get_default_headers(client._connection.user_agent, content_type),
2628+
)
26182629
else:
2619-
self.assertEqual(upload._headers, extra_headers)
2620-
self.assertIsNot(upload._headers, extra_headers)
2630+
expected_headers = {
2631+
**_get_default_headers(client._connection.user_agent, content_type),
2632+
**extra_headers,
2633+
}
2634+
self.assertEqual(upload._headers, expected_headers)
2635+
self.assertIsNot(upload._headers, expected_headers)
26212636
self.assertFalse(upload.finished)
26222637
if chunk_size is None:
26232638
if blob_chunk_size is None:
@@ -2656,10 +2671,9 @@ def _initiate_resumable_helper(
26562671
# Check the mocks.
26572672
blob._get_writable_metadata.assert_called_once_with()
26582673
payload = json.dumps(object_metadata).encode("utf-8")
2659-
expected_headers = {
2660-
"content-type": "application/json; charset=UTF-8",
2661-
"x-upload-content-type": content_type,
2662-
}
2674+
expected_headers = _get_default_headers(
2675+
client._connection.user_agent, x_upload_content_type=content_type
2676+
)
26632677
if size is not None:
26642678
expected_headers["x-upload-content-length"] = str(size)
26652679
if extra_headers is not None:
@@ -2778,6 +2792,7 @@ def _make_resumable_transport(
27782792

27792793
@staticmethod
27802794
def _do_resumable_upload_call0(
2795+
client,
27812796
blob,
27822797
content_type,
27832798
size=None,
@@ -2796,10 +2811,9 @@ def _do_resumable_upload_call0(
27962811
)
27972812
if predefined_acl is not None:
27982813
upload_url += "&predefinedAcl={}".format(predefined_acl)
2799-
expected_headers = {
2800-
"content-type": "application/json; charset=UTF-8",
2801-
"x-upload-content-type": content_type,
2802-
}
2814+
expected_headers = _get_default_headers(
2815+
client._connection.user_agent, x_upload_content_type=content_type
2816+
)
28032817
if size is not None:
28042818
expected_headers["x-upload-content-length"] = str(size)
28052819
payload = json.dumps({"name": blob.name}).encode("utf-8")
@@ -2809,6 +2823,7 @@ def _do_resumable_upload_call0(
28092823

28102824
@staticmethod
28112825
def _do_resumable_upload_call1(
2826+
client,
28122827
blob,
28132828
content_type,
28142829
data,
@@ -2828,6 +2843,9 @@ def _do_resumable_upload_call1(
28282843
content_range = "bytes 0-{:d}/{:d}".format(blob.chunk_size - 1, size)
28292844

28302845
expected_headers = {
2846+
**_get_default_headers(
2847+
client._connection.user_agent, x_upload_content_type=content_type
2848+
),
28312849
"content-type": content_type,
28322850
"content-range": content_range,
28332851
}
@@ -2842,6 +2860,7 @@ def _do_resumable_upload_call1(
28422860

28432861
@staticmethod
28442862
def _do_resumable_upload_call2(
2863+
client,
28452864
blob,
28462865
content_type,
28472866
data,
@@ -2859,6 +2878,9 @@ def _do_resumable_upload_call2(
28592878
blob.chunk_size, total_bytes - 1, total_bytes
28602879
)
28612880
expected_headers = {
2881+
**_get_default_headers(
2882+
client._connection.user_agent, x_upload_content_type=content_type
2883+
),
28622884
"content-type": content_type,
28632885
"content-range": content_range,
28642886
}
@@ -2884,13 +2906,11 @@ def _do_resumable_helper(
28842906
data_corruption=False,
28852907
retry=None,
28862908
):
2887-
bucket = _Bucket(name="yesterday")
2888-
blob = self._make_one(u"blob-name", bucket=bucket)
2889-
blob.chunk_size = blob._CHUNK_SIZE_MULTIPLE
2890-
self.assertIsNotNone(blob.chunk_size)
2891-
2909+
CHUNK_SIZE = 256 * 1024
2910+
USER_AGENT = "testing 1.2.3"
2911+
content_type = u"text/html"
28922912
# Data to be uploaded.
2893-
data = b"<html>" + (b"A" * blob.chunk_size) + b"</html>"
2913+
data = b"<html>" + (b"A" * CHUNK_SIZE) + b"</html>"
28942914
total_bytes = len(data)
28952915
if use_size:
28962916
size = total_bytes
@@ -2899,17 +2919,29 @@ def _do_resumable_helper(
28992919

29002920
# Create mocks to be checked for doing transport.
29012921
resumable_url = "http://test.invalid?upload_id=and-then-there-was-1"
2902-
headers1 = {"location": resumable_url}
2903-
headers2 = {"range": "bytes=0-{:d}".format(blob.chunk_size - 1)}
2922+
headers1 = {
2923+
**_get_default_headers(USER_AGENT, content_type),
2924+
"location": resumable_url,
2925+
}
2926+
headers2 = {
2927+
**_get_default_headers(USER_AGENT, content_type),
2928+
"range": "bytes=0-{:d}".format(CHUNK_SIZE - 1),
2929+
}
2930+
headers3 = _get_default_headers(USER_AGENT, content_type)
29042931
transport, responses = self._make_resumable_transport(
2905-
headers1, headers2, {}, total_bytes, data_corruption=data_corruption
2932+
headers1, headers2, headers3, total_bytes, data_corruption=data_corruption
29062933
)
29072934

29082935
# Create some mock arguments and call the method under test.
29092936
client = mock.Mock(_http=transport, _connection=_Connection, spec=["_http"])
29102937
client._connection.API_BASE_URL = "https://storage.googleapis.com"
2938+
client._connection.user_agent = USER_AGENT
29112939
stream = io.BytesIO(data)
2912-
content_type = u"text/html"
2940+
2941+
bucket = _Bucket(name="yesterday")
2942+
blob = self._make_one(u"blob-name", bucket=bucket)
2943+
blob.chunk_size = blob._CHUNK_SIZE_MULTIPLE
2944+
self.assertIsNotNone(blob.chunk_size)
29132945

29142946
if timeout is None:
29152947
expected_timeout = self._get_default_timeout()
@@ -2939,6 +2971,7 @@ def _do_resumable_helper(
29392971

29402972
# Check the mocks.
29412973
call0 = self._do_resumable_upload_call0(
2974+
client,
29422975
blob,
29432976
content_type,
29442977
size=size,
@@ -2950,6 +2983,7 @@ def _do_resumable_helper(
29502983
timeout=expected_timeout,
29512984
)
29522985
call1 = self._do_resumable_upload_call1(
2986+
client,
29532987
blob,
29542988
content_type,
29552989
data,
@@ -2963,6 +2997,7 @@ def _do_resumable_helper(
29632997
timeout=expected_timeout,
29642998
)
29652999
call2 = self._do_resumable_upload_call2(
3000+
client,
29663001
blob,
29673002
content_type,
29683003
data,
@@ -3510,6 +3545,7 @@ def _create_resumable_upload_session_helper(
35103545
size = 10000
35113546
client = mock.Mock(_http=transport, _connection=_Connection, spec=[u"_http"])
35123547
client._connection.API_BASE_URL = "https://storage.googleapis.com"
3548+
client._connection.user_agent = "testing 1.2.3"
35133549

35143550
if timeout is None:
35153551
expected_timeout = self._get_default_timeout()
@@ -3556,7 +3592,9 @@ def _create_resumable_upload_session_helper(
35563592
upload_url += "?" + urlencode(qs_params)
35573593
payload = b'{"name": "blob-name"}'
35583594
expected_headers = {
3559-
"content-type": "application/json; charset=UTF-8",
3595+
**_get_default_headers(
3596+
client._connection.user_agent, x_upload_content_type=content_type
3597+
),
35603598
"x-upload-content-length": str(size),
35613599
"x-upload-content-type": content_type,
35623600
}
@@ -5739,6 +5777,7 @@ class _Connection(object):
57395777

57405778
API_BASE_URL = "http://example.com"
57415779
USER_AGENT = "testing 1.2.3"
5780+
user_agent = "testing 1.2.3"
57425781
credentials = object()
57435782

57445783

tests/unit/test_client.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from google.oauth2.service_account import Credentials
2929

3030
from google.cloud.storage._helpers import STORAGE_EMULATOR_ENV_VAR
31+
from google.cloud.storage._helpers import _get_default_headers
3132
from google.cloud.storage.retry import DEFAULT_RETRY
3233
from google.cloud.storage.retry import DEFAULT_RETRY_IF_GENERATION_SPECIFIED
3334

@@ -1567,7 +1568,10 @@ def test_download_blob_to_file_with_failure(self):
15671568

15681569
self.assertEqual(file_obj.tell(), 0)
15691570

1570-
headers = {"accept-encoding": "gzip"}
1571+
headers = {
1572+
**_get_default_headers(client._connection.user_agent),
1573+
"accept-encoding": "gzip",
1574+
}
15711575
blob._do_download.assert_called_once_with(
15721576
client._http,
15731577
file_obj,
@@ -1598,7 +1602,10 @@ def test_download_blob_to_file_with_uri(self):
15981602
):
15991603
client.download_blob_to_file("gs://bucket_name/path/to/object", file_obj)
16001604

1601-
headers = {"accept-encoding": "gzip"}
1605+
headers = {
1606+
**_get_default_headers(client._connection.user_agent),
1607+
"accept-encoding": "gzip",
1608+
}
16021609
blob._do_download.assert_called_once_with(
16031610
client._http,
16041611
file_obj,
@@ -1714,6 +1721,7 @@ def _download_blob_to_file_helper(
17141721
if_etag_not_match = [if_etag_not_match]
17151722
headers["If-None-Match"] = ", ".join(if_etag_not_match)
17161723

1724+
headers = {**_get_default_headers(client._connection.user_agent), **headers}
17171725
blob._do_download.assert_called_once_with(
17181726
client._http,
17191727
file_obj,

0 commit comments

Comments
 (0)