Skip to content

Commit dbaf058

Browse files
authored
feat: add timeout parameter to Batch interface to match google-cloud-core (#10010)
* fix: increase minimum version of google-cloud-core after a required field is introduced * feat: extend batch interface to have timeout to match new google-cloud-core interface
1 parent ebedc05 commit dbaf058

File tree

5 files changed

+77
-38
lines changed

5 files changed

+77
-38
lines changed

google/cloud/storage/batch.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def __init__(self, client):
150150
self._requests = []
151151
self._target_objects = []
152152

153-
def _do_request(self, method, url, headers, data, target_object):
153+
def _do_request(self, method, url, headers, data, target_object, timeout=None):
154154
"""Override Connection: defer actual HTTP request.
155155
156156
Only allow up to ``_MAX_BATCH_SIZE`` requests to be deferred.
@@ -173,6 +173,12 @@ def _do_request(self, method, url, headers, data, target_object):
173173
connection. Here we defer an HTTP request and complete
174174
initialization of the object at a later time.
175175
176+
:type timeout: float or tuple
177+
:param timeout: (optional) The amount of time, in seconds, to wait
178+
for the server response. By default, the method waits indefinitely.
179+
Can also be passed as a tuple (connect_timeout, read_timeout).
180+
See :meth:`requests.Session.request` documentation for details.
181+
176182
:rtype: tuple of ``response`` (a dictionary of sorts)
177183
and ``content`` (a string).
178184
:returns: The HTTP response object and the content of the response.
@@ -181,7 +187,7 @@ def _do_request(self, method, url, headers, data, target_object):
181187
raise ValueError(
182188
"Too many deferred requests (max %d)" % self._MAX_BATCH_SIZE
183189
)
184-
self._requests.append((method, url, headers, data))
190+
self._requests.append((method, url, headers, data, timeout))
185191
result = _FutureDict()
186192
self._target_objects.append(target_object)
187193
if target_object is not None:
@@ -200,9 +206,12 @@ def _prepare_batch_request(self):
200206

201207
multi = MIMEMultipart()
202208

203-
for method, uri, headers, body in self._requests:
209+
# Use timeout of last request, default to None (indefinite)
210+
timeout = None
211+
for method, uri, headers, body, _timeout in self._requests:
204212
subrequest = MIMEApplicationHTTP(method, uri, headers, body)
205213
multi.attach(subrequest)
214+
timeout = _timeout
206215

207216
# The `email` package expects to deal with "native" strings
208217
if six.PY3: # pragma: NO COVER Python3
@@ -215,7 +224,7 @@ def _prepare_batch_request(self):
215224

216225
# Strip off redundant header text
217226
_, body = payload.split("\n\n", 1)
218-
return dict(multi._headers), body
227+
return dict(multi._headers), body, timeout
219228

220229
def _finish_futures(self, responses):
221230
"""Apply all the batch responses to the futures created.
@@ -230,7 +239,7 @@ def _finish_futures(self, responses):
230239
# until all futures have been populated.
231240
exception_args = None
232241

233-
if len(self._target_objects) != len(responses):
242+
if len(self._target_objects) != len(responses): # pragma: NO COVER
234243
raise ValueError("Expected a response for every request.")
235244

236245
for target_object, subresponse in zip(self._target_objects, responses):
@@ -251,15 +260,15 @@ def finish(self):
251260
:rtype: list of tuples
252261
:returns: one ``(headers, payload)`` tuple per deferred request.
253262
"""
254-
headers, body = self._prepare_batch_request()
263+
headers, body, timeout = self._prepare_batch_request()
255264

256265
url = "%s/batch/storage/v1" % self.API_BASE_URL
257266

258267
# Use the private ``_base_connection`` rather than the property
259268
# ``_connection``, since the property may be this
260269
# current batch.
261270
response = self._client._base_connection._make_request(
262-
"POST", url, data=body, headers=headers
271+
"POST", url, data=body, headers=headers, timeout=timeout
263272
)
264273
responses = list(_unpack_batch_response(response))
265274
self._finish_futures(responses)
@@ -313,7 +322,7 @@ def _unpack_batch_response(response):
313322
parser = Parser()
314323
message = _generate_faux_mime_message(parser, response)
315324

316-
if not isinstance(message._payload, list):
325+
if not isinstance(message._payload, list): # pragma: NO COVER
317326
raise ValueError("Bad response: not multi-part")
318327

319328
for subrequest in message._payload:

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
# 'Development Status :: 5 - Production/Stable'
3030
release_status = "Development Status :: 5 - Production/Stable"
3131
dependencies = [
32-
"google-auth >= 1.2.0",
33-
"google-cloud-core >= 1.0.3, < 2.0dev",
32+
"google-auth >= 1.9.0, < 2.0dev",
33+
"google-cloud-core >= 1.1.0, < 2.0dev",
3434
"google-resumable-media >= 0.5.0, < 0.6dev",
3535
]
3636
extras = {}

tests/unit/test__http.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ def test_extra_headers(self):
5151
}
5252
expected_uri = conn.build_api_url("/rainbow")
5353
http.request.assert_called_once_with(
54-
data=req_data, headers=expected_headers, method="GET", url=expected_uri
54+
data=req_data,
55+
headers=expected_headers,
56+
method="GET",
57+
url=expected_uri,
58+
timeout=None,
5559
)
5660

5761
def test_build_api_url_no_extra_query_params(self):

tests/unit/test_batch.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def test__make_request_GET_normal(self):
147147
# Check the queued request
148148
self.assertEqual(len(batch._requests), 1)
149149
request = batch._requests[0]
150-
request_method, request_url, _, request_data = request
150+
request_method, request_url, _, request_data, _ = request
151151
self.assertEqual(request_method, "GET")
152152
self.assertEqual(request_url, url)
153153
self.assertIsNone(request_data)
@@ -174,7 +174,7 @@ def test__make_request_POST_normal(self):
174174
http.request.assert_not_called()
175175

176176
request = batch._requests[0]
177-
request_method, request_url, _, request_data = request
177+
request_method, request_url, _, request_data, _ = request
178178
self.assertEqual(request_method, "POST")
179179
self.assertEqual(request_url, url)
180180
self.assertEqual(request_data, data)
@@ -201,7 +201,7 @@ def test__make_request_PATCH_normal(self):
201201
http.request.assert_not_called()
202202

203203
request = batch._requests[0]
204-
request_method, request_url, _, request_data = request
204+
request_method, request_url, _, request_data, _ = request
205205
self.assertEqual(request_method, "PATCH")
206206
self.assertEqual(request_url, url)
207207
self.assertEqual(request_data, data)
@@ -228,7 +228,7 @@ def test__make_request_DELETE_normal(self):
228228
# Check the queued request
229229
self.assertEqual(len(batch._requests), 1)
230230
request = batch._requests[0]
231-
request_method, request_url, _, request_data = request
231+
request_method, request_url, _, request_data, _ = request
232232
self.assertEqual(request_method, "DELETE")
233233
self.assertEqual(request_url, url)
234234
self.assertIsNone(request_data)
@@ -340,7 +340,11 @@ def test_finish_nonempty(self):
340340

341341
expected_url = "{}/batch/storage/v1".format(batch.API_BASE_URL)
342342
http.request.assert_called_once_with(
343-
method="POST", url=expected_url, headers=mock.ANY, data=mock.ANY
343+
method="POST",
344+
url=expected_url,
345+
headers=mock.ANY,
346+
data=mock.ANY,
347+
timeout=mock.ANY,
344348
)
345349

346350
request_info = self._get_mutlipart_request(http)
@@ -406,7 +410,11 @@ def test_finish_nonempty_with_status_failure(self):
406410

407411
expected_url = "{}/batch/storage/v1".format(batch.API_BASE_URL)
408412
http.request.assert_called_once_with(
409-
method="POST", url=expected_url, headers=mock.ANY, data=mock.ANY
413+
method="POST",
414+
url=expected_url,
415+
headers=mock.ANY,
416+
data=mock.ANY,
417+
timeout=mock.ANY,
410418
)
411419

412420
_, request_body, _, boundary = self._get_mutlipart_request(http)
@@ -620,8 +628,10 @@ class _Connection(object):
620628
def __init__(self, **kw):
621629
self.__dict__.update(kw)
622630

623-
def _make_request(self, method, url, data=None, headers=None):
624-
return self.http.request(url=url, method=method, headers=headers, data=data)
631+
def _make_request(self, method, url, data=None, headers=None, timeout=None):
632+
return self.http.request(
633+
url=url, method=method, headers=headers, data=data, timeout=timeout
634+
)
625635

626636

627637
class _MockObject(object):

tests/unit/test_client.py

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ def test_get_service_account_email_wo_project(self):
271271
]
272272
)
273273
http.request.assert_called_once_with(
274-
method="GET", url=URI, data=None, headers=mock.ANY
274+
method="GET", url=URI, data=None, headers=mock.ANY, timeout=mock.ANY
275275
)
276276

277277
def test_get_service_account_email_w_project(self):
@@ -297,7 +297,7 @@ def test_get_service_account_email_w_project(self):
297297
]
298298
)
299299
http.request.assert_called_once_with(
300-
method="GET", url=URI, data=None, headers=mock.ANY
300+
method="GET", url=URI, data=None, headers=mock.ANY, timeout=mock.ANY
301301
)
302302

303303
def test_bucket(self):
@@ -366,7 +366,7 @@ def test_get_bucket_with_string_miss(self):
366366
client.get_bucket(NONESUCH)
367367

368368
http.request.assert_called_once_with(
369-
method="GET", url=URI, data=mock.ANY, headers=mock.ANY
369+
method="GET", url=URI, data=mock.ANY, headers=mock.ANY, timeout=mock.ANY
370370
)
371371

372372
def test_get_bucket_with_string_hit(self):
@@ -396,7 +396,7 @@ def test_get_bucket_with_string_hit(self):
396396
self.assertIsInstance(bucket, Bucket)
397397
self.assertEqual(bucket.name, BUCKET_NAME)
398398
http.request.assert_called_once_with(
399-
method="GET", url=URI, data=mock.ANY, headers=mock.ANY
399+
method="GET", url=URI, data=mock.ANY, headers=mock.ANY, timeout=mock.ANY
400400
)
401401

402402
def test_get_bucket_with_object_miss(self):
@@ -427,7 +427,7 @@ def test_get_bucket_with_object_miss(self):
427427
client.get_bucket(bucket_obj)
428428

429429
http.request.assert_called_once_with(
430-
method="GET", url=URI, data=mock.ANY, headers=mock.ANY
430+
method="GET", url=URI, data=mock.ANY, headers=mock.ANY, timeout=mock.ANY
431431
)
432432

433433
def test_get_bucket_with_object_hit(self):
@@ -458,7 +458,7 @@ def test_get_bucket_with_object_hit(self):
458458
self.assertIsInstance(bucket, Bucket)
459459
self.assertEqual(bucket.name, bucket_name)
460460
http.request.assert_called_once_with(
461-
method="GET", url=URI, data=mock.ANY, headers=mock.ANY
461+
method="GET", url=URI, data=mock.ANY, headers=mock.ANY, timeout=mock.ANY
462462
)
463463

464464
def test_lookup_bucket_miss(self):
@@ -485,7 +485,7 @@ def test_lookup_bucket_miss(self):
485485

486486
self.assertIsNone(bucket)
487487
http.request.assert_called_once_with(
488-
method="GET", url=URI, data=mock.ANY, headers=mock.ANY
488+
method="GET", url=URI, data=mock.ANY, headers=mock.ANY, timeout=mock.ANY
489489
)
490490

491491
def test_lookup_bucket_hit(self):
@@ -514,7 +514,7 @@ def test_lookup_bucket_hit(self):
514514
self.assertIsInstance(bucket, Bucket)
515515
self.assertEqual(bucket.name, BUCKET_NAME)
516516
http.request.assert_called_once_with(
517-
method="GET", url=URI, data=mock.ANY, headers=mock.ANY
517+
method="GET", url=URI, data=mock.ANY, headers=mock.ANY, timeout=mock.ANY
518518
)
519519

520520
def test_create_bucket_w_missing_client_project(self):
@@ -666,7 +666,7 @@ def test_create_bucket_w_string_success(self):
666666
self.assertEqual(bucket.name, bucket_name)
667667
self.assertTrue(bucket.requester_pays)
668668
http.request.assert_called_once_with(
669-
method="POST", url=URI, data=mock.ANY, headers=mock.ANY
669+
method="POST", url=URI, data=mock.ANY, headers=mock.ANY, timeout=mock.ANY
670670
)
671671
json_sent = http.request.call_args_list[0][1]["data"]
672672
self.assertEqual(json_expected, json.loads(json_sent))
@@ -706,7 +706,7 @@ def test_create_bucket_w_object_success(self):
706706
self.assertEqual(bucket.name, bucket_name)
707707
self.assertTrue(bucket.requester_pays)
708708
http.request.assert_called_once_with(
709-
method="POST", url=URI, data=mock.ANY, headers=mock.ANY
709+
method="POST", url=URI, data=mock.ANY, headers=mock.ANY, timeout=mock.ANY
710710
)
711711
json_sent = http.request.call_args_list[0][1]["data"]
712712
self.assertEqual(json_expected, json.loads(json_sent))
@@ -848,7 +848,11 @@ def test_list_buckets_empty(self):
848848
self.assertEqual(len(buckets), 0)
849849

850850
http.request.assert_called_once_with(
851-
method="GET", url=mock.ANY, data=mock.ANY, headers=mock.ANY
851+
method="GET",
852+
url=mock.ANY,
853+
data=mock.ANY,
854+
headers=mock.ANY,
855+
timeout=mock.ANY,
852856
)
853857

854858
requested_url = http.request.mock_calls[0][2]["url"]
@@ -883,7 +887,11 @@ def test_list_buckets_explicit_project(self):
883887
self.assertEqual(len(buckets), 0)
884888

885889
http.request.assert_called_once_with(
886-
method="GET", url=mock.ANY, data=mock.ANY, headers=mock.ANY
890+
method="GET",
891+
url=mock.ANY,
892+
data=mock.ANY,
893+
headers=mock.ANY,
894+
timeout=mock.ANY,
887895
)
888896

889897
requested_url = http.request.mock_calls[0][2]["url"]
@@ -918,7 +926,11 @@ def test_list_buckets_non_empty(self):
918926
self.assertEqual(buckets[0].name, BUCKET_NAME)
919927

920928
http.request.assert_called_once_with(
921-
method="GET", url=mock.ANY, data=mock.ANY, headers=mock.ANY
929+
method="GET",
930+
url=mock.ANY,
931+
data=mock.ANY,
932+
headers=mock.ANY,
933+
timeout=mock.ANY,
922934
)
923935

924936
def test_list_buckets_all_arguments(self):
@@ -948,7 +960,11 @@ def test_list_buckets_all_arguments(self):
948960
buckets = list(iterator)
949961
self.assertEqual(buckets, [])
950962
http.request.assert_called_once_with(
951-
method="GET", url=mock.ANY, data=mock.ANY, headers=mock.ANY
963+
method="GET",
964+
url=mock.ANY,
965+
data=mock.ANY,
966+
headers=mock.ANY,
967+
timeout=mock.ANY,
952968
)
953969

954970
requested_url = http.request.mock_calls[0][2]["url"]
@@ -1077,7 +1093,7 @@ def _create_hmac_key_helper(self, explicit_project=None, user_project=None):
10771093

10781094
FULL_URI = "{}?{}".format(URI, urlencode(qs_params))
10791095
http.request.assert_called_once_with(
1080-
method="POST", url=FULL_URI, data=None, headers=mock.ANY
1096+
method="POST", url=FULL_URI, data=None, headers=mock.ANY, timeout=mock.ANY
10811097
)
10821098

10831099
def test_create_hmac_key_defaults(self):
@@ -1112,7 +1128,7 @@ def test_list_hmac_keys_defaults_empty(self):
11121128
]
11131129
)
11141130
http.request.assert_called_once_with(
1115-
method="GET", url=URI, data=None, headers=mock.ANY
1131+
method="GET", url=URI, data=None, headers=mock.ANY, timeout=mock.ANY
11161132
)
11171133

11181134
def test_list_hmac_keys_explicit_non_empty(self):
@@ -1176,7 +1192,7 @@ def test_list_hmac_keys_explicit_non_empty(self):
11761192
"userProject": USER_PROJECT,
11771193
}
11781194
http.request.assert_called_once_with(
1179-
method="GET", url=mock.ANY, data=None, headers=mock.ANY
1195+
method="GET", url=mock.ANY, data=None, headers=mock.ANY, timeout=mock.ANY
11801196
)
11811197
kwargs = http.request.mock_calls[0].kwargs
11821198
uri = kwargs["url"]
@@ -1223,7 +1239,7 @@ def test_get_hmac_key_metadata_wo_project(self):
12231239
]
12241240
)
12251241
http.request.assert_called_once_with(
1226-
method="GET", url=URI, data=None, headers=mock.ANY
1242+
method="GET", url=URI, data=None, headers=mock.ANY, timeout=mock.ANY
12271243
)
12281244

12291245
def test_get_hmac_key_metadata_w_project(self):
@@ -1273,5 +1289,5 @@ def test_get_hmac_key_metadata_w_project(self):
12731289
FULL_URI = "{}?{}".format(URI, urlencode(qs_params))
12741290

12751291
http.request.assert_called_once_with(
1276-
method="GET", url=FULL_URI, data=None, headers=mock.ANY
1292+
method="GET", url=FULL_URI, data=None, headers=mock.ANY, timeout=mock.ANY
12771293
)

0 commit comments

Comments
 (0)