Skip to content

Commit 976c5f4

Browse files
author
Jon Wayne Parrott
committed
Storage: replace httplib2 with Requests
1 parent 2bc9846 commit 976c5f4

File tree

6 files changed

+432
-400
lines changed

6 files changed

+432
-400
lines changed

storage/google/cloud/storage/batch.py

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@
2323
import io
2424
import json
2525

26-
import httplib2
26+
import requests
2727
import six
2828

29-
from google.cloud.exceptions import make_exception
29+
from google.cloud import exceptions
3030
from google.cloud.storage._http import Connection
3131

3232

@@ -70,11 +70,6 @@ def __init__(self, method, uri, headers, body):
7070
super_init(payload, 'http', encode_noop)
7171

7272

73-
class NoContent(object):
74-
"""Emulate an HTTP '204 No Content' response."""
75-
status = 204
76-
77-
7873
class _FutureDict(object):
7974
"""Class to hold a future value for a deferred request.
8075
@@ -123,6 +118,19 @@ def __setitem__(self, key, value):
123118
raise KeyError('Cannot set %r -> %r on a future' % (key, value))
124119

125120

121+
class _FutureResponse(requests.Response):
122+
def __init__(self, future_dict):
123+
super(_FutureResponse, self).__init__()
124+
self._future_dict = future_dict
125+
self.status_code = 204
126+
127+
def json(self):
128+
raise ValueError()
129+
130+
def content(self):
131+
return self._future_dict
132+
133+
126134
class Batch(Connection):
127135
"""Proxy an underlying connection, batching up change operations.
128136
@@ -171,7 +179,7 @@ def _do_request(self, method, url, headers, data, target_object):
171179
self._target_objects.append(target_object)
172180
if target_object is not None:
173181
target_object._properties = result
174-
return NoContent(), result
182+
return _FutureResponse(result)
175183

176184
def _prepare_batch_request(self):
177185
"""Prepares headers and body for a batch request.
@@ -218,17 +226,18 @@ def _finish_futures(self, responses):
218226
if len(self._target_objects) != len(responses):
219227
raise ValueError('Expected a response for every request.')
220228

221-
for target_object, sub_response in zip(self._target_objects,
222-
responses):
223-
resp_headers, sub_payload = sub_response
224-
if not 200 <= resp_headers.status < 300:
225-
exception_args = exception_args or (resp_headers,
226-
sub_payload)
229+
for target_object, subresponse in zip(
230+
self._target_objects, responses):
231+
if not 200 <= subresponse.status_code < 300:
232+
exception_args = exception_args or subresponse
227233
elif target_object is not None:
228-
target_object._properties = sub_payload
234+
try:
235+
target_object._properties = subresponse.json()
236+
except ValueError:
237+
target_object._properties = subresponse.content
229238

230239
if exception_args is not None:
231-
raise make_exception(*exception_args)
240+
raise exceptions.from_http_response(exception_args)
232241

233242
def finish(self):
234243
"""Submit a single `multipart/mixed` request with deferred requests.
@@ -243,9 +252,9 @@ def finish(self):
243252
# Use the private ``_base_connection`` rather than the property
244253
# ``_connection``, since the property may be this
245254
# current batch.
246-
response, content = self._client._base_connection._make_request(
255+
response = self._client._base_connection._make_request(
247256
'POST', url, data=body, headers=headers)
248-
responses = list(_unpack_batch_response(response, content))
257+
responses = list(_unpack_batch_response(response))
249258
self._finish_futures(responses)
250259
return responses
251260

@@ -265,24 +274,23 @@ def __exit__(self, exc_type, exc_val, exc_tb):
265274
self._client._pop_batch()
266275

267276

268-
def _generate_faux_mime_message(parser, response, content):
277+
def _generate_faux_mime_message(parser, response):
269278
"""Convert response, content -> (multipart) email.message.
270279
271280
Helper for _unpack_batch_response.
272281
"""
273282
# We coerce to bytes to get consistent concat across
274283
# Py2 and Py3. Percent formatting is insufficient since
275284
# it includes the b in Py3.
276-
if not isinstance(content, six.binary_type):
277-
content = content.encode('utf-8')
278-
content_type = response['content-type']
285+
content_type = response.headers.get('content-type', '')
279286
if not isinstance(content_type, six.binary_type):
280287
content_type = content_type.encode('utf-8')
288+
281289
faux_message = b''.join([
282290
b'Content-Type: ',
283291
content_type,
284292
b'\nMIME-Version: 1.0\n\n',
285-
content,
293+
response.content,
286294
])
287295

288296
if six.PY2:
@@ -291,20 +299,17 @@ def _generate_faux_mime_message(parser, response, content):
291299
return parser.parsestr(faux_message.decode('utf-8'))
292300

293301

294-
def _unpack_batch_response(response, content):
295-
"""Convert response, content -> [(headers, payload)].
302+
def _unpack_batch_response(response):
303+
"""Convert requests.Response -> [(headers, payload)].
296304
297305
Creates a generator of tuples of emulating the responses to
298306
:meth:`httplib2.Http.request` (a pair of headers and payload).
299307
300-
:type response: :class:`httplib2.Response`
308+
:type response: :class:`requests.Response`
301309
:param response: HTTP response / headers from a request.
302-
303-
:type content: str
304-
:param content: Response payload with a batch response.
305310
"""
306311
parser = Parser()
307-
message = _generate_faux_mime_message(parser, response, content)
312+
message = _generate_faux_mime_message(parser, response)
308313

309314
if not isinstance(message._payload, list):
310315
raise ValueError('Bad response: not multi-part')
@@ -314,10 +319,15 @@ def _unpack_batch_response(response, content):
314319
_, status, _ = status_line.split(' ', 2)
315320
sub_message = parser.parsestr(rest)
316321
payload = sub_message._payload
317-
ctype = sub_message['Content-Type']
318322
msg_headers = dict(sub_message._headers)
319-
msg_headers['status'] = status
320-
headers = httplib2.Response(msg_headers)
321-
if ctype and ctype.startswith('application/json'):
322-
payload = json.loads(payload)
323-
yield headers, payload
323+
content_id = msg_headers.get('Content-ID')
324+
325+
subresponse = requests.Response()
326+
subresponse.request = requests.Request(
327+
method='BATCH',
328+
url='contentid://{}'.format(content_id)).prepare()
329+
subresponse.status_code = int(status)
330+
subresponse.headers.update(msg_headers)
331+
subresponse._content = payload.encode('utf-8')
332+
333+
yield subresponse

storage/google/cloud/storage/blob.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import time
3535
import warnings
3636

37-
import httplib2
3837
from six.moves.urllib.parse import quote
3938

4039
import google.auth.transport.requests
@@ -44,11 +43,11 @@
4443
from google.resumable_media.requests import MultipartUpload
4544
from google.resumable_media.requests import ResumableUpload
4645

46+
from google.cloud import exceptions
4747
from google.cloud._helpers import _rfc3339_to_datetime
4848
from google.cloud._helpers import _to_bytes
4949
from google.cloud._helpers import _bytes_to_unicode
5050
from google.cloud.exceptions import NotFound
51-
from google.cloud.exceptions import make_exception
5251
from google.cloud.iam import Policy
5352
from google.cloud.storage._helpers import _PropertyMixin
5453
from google.cloud.storage._helpers import _scalar_property
@@ -469,7 +468,7 @@ def download_to_file(self, file_obj, client=None):
469468
try:
470469
self._do_download(transport, file_obj, download_url, headers)
471470
except resumable_media.InvalidResponse as exc:
472-
_raise_from_invalid_response(exc, download_url)
471+
_raise_from_invalid_response(exc)
473472

474473
def download_to_filename(self, filename, client=None):
475474
"""Download the contents of this blob into a named file.
@@ -1598,20 +1597,14 @@ def _maybe_rewind(stream, rewind=False):
15981597
stream.seek(0, os.SEEK_SET)
15991598

16001599

1601-
def _raise_from_invalid_response(error, error_info=None):
1600+
def _raise_from_invalid_response(error):
16021601
"""Re-wrap and raise an ``InvalidResponse`` exception.
16031602
16041603
:type error: :exc:`google.resumable_media.InvalidResponse`
16051604
:param error: A caught exception from the ``google-resumable-media``
16061605
library.
16071606
1608-
:type error_info: str
1609-
:param error_info: (Optional) Extra information about the failed request.
1610-
16111607
:raises: :class:`~google.cloud.exceptions.GoogleCloudError` corresponding
16121608
to the failed status code
16131609
"""
1614-
response = error.response
1615-
faux_response = httplib2.Response({'status': response.status_code})
1616-
raise make_exception(faux_response, response.content,
1617-
error_info=error_info, use_json=False)
1610+
raise exceptions.from_http_response(error.response)

storage/tests/unit/test__http.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,17 @@ def _make_one(self, *args, **kw):
2929
return self._get_target_class()(*args, **kw)
3030

3131
def test_extra_headers(self):
32+
import requests
33+
3234
from google.cloud import _http as base_http
3335
from google.cloud.storage import _http as MUT
3436

35-
http = mock.Mock(spec=['request'])
36-
response = mock.Mock(status=200, spec=['status'])
37+
http = mock.create_autospec(requests.Session, instance=True)
38+
response = requests.Response()
39+
response.status_code = 200
3740
data = b'brent-spiner'
38-
http.request.return_value = response, data
41+
response._content = data
42+
http.request.return_value = response
3943
client = mock.Mock(_http=http, spec=['_http'])
4044

4145
conn = self._make_one(client)
@@ -45,17 +49,16 @@ def test_extra_headers(self):
4549
self.assertEqual(result, data)
4650

4751
expected_headers = {
48-
'Content-Length': str(len(req_data)),
4952
'Accept-Encoding': 'gzip',
5053
base_http.CLIENT_INFO_HEADER: MUT._CLIENT_INFO,
5154
'User-Agent': conn.USER_AGENT,
5255
}
5356
expected_uri = conn.build_api_url('/rainbow')
5457
http.request.assert_called_once_with(
55-
body=req_data,
58+
data=req_data,
5659
headers=expected_headers,
5760
method='GET',
58-
uri=expected_uri,
61+
url=expected_uri,
5962
)
6063

6164
def test_build_api_url_no_extra_query_params(self):

0 commit comments

Comments
 (0)