Skip to content

Commit b461085

Browse files
committed
Add 'Bucket.requester_pays' property. (#3488)
Also, add 'requester_pays' argument to 'Client.create_bucket'. Add a system test which exercises the feature. Note that the new system test is skipped, because 'Buckets.insert' fails with the 'billing/requesterPays' field set, both in our system tests and in the 'Try It!' form in the docs. Toward #3474.
1 parent 3123de6 commit b461085

5 files changed

Lines changed: 95 additions & 19 deletions

File tree

storage/google/cloud/storage/bucket.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -897,10 +897,40 @@ def versioning_enabled(self, value):
897897
details.
898898
899899
:type value: convertible to boolean
900-
:param value: should versioning be anabled for the bucket?
900+
:param value: should versioning be enabled for the bucket?
901901
"""
902902
self._patch_property('versioning', {'enabled': bool(value)})
903903

904+
@property
905+
def requester_pays(self):
906+
"""Does the requester pay for API requests for this bucket?
907+
908+
.. note::
909+
910+
No public docs exist yet for the "requester pays" feature.
911+
912+
:setter: Update whether requester pays for this bucket.
913+
:getter: Query whether requester pays for this bucket.
914+
915+
:rtype: bool
916+
:returns: True if requester pays for API requests for the bucket,
917+
else False.
918+
"""
919+
versioning = self._properties.get('billing', {})
920+
return versioning.get('requesterPays', False)
921+
922+
@requester_pays.setter
923+
def requester_pays(self, value):
924+
"""Update whether requester pays for API requests for this bucket.
925+
926+
See https://cloud.google.com/storage/docs/<DOCS-MISSING> for
927+
details.
928+
929+
:type value: convertible to boolean
930+
:param value: should requester pay for API requests for the bucket?
931+
"""
932+
self._patch_property('billing', {'requesterPays': bool(value)})
933+
904934
def configure_website(self, main_page_suffix=None, not_found_page=None):
905935
"""Configure website-related properties.
906936

storage/google/cloud/storage/client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def lookup_bucket(self, bucket_name):
194194
except NotFound:
195195
return None
196196

197-
def create_bucket(self, bucket_name):
197+
def create_bucket(self, bucket_name, requester_pays=None):
198198
"""Create a new bucket.
199199
200200
For example:
@@ -211,10 +211,17 @@ def create_bucket(self, bucket_name):
211211
:type bucket_name: str
212212
:param bucket_name: The bucket name to create.
213213
214+
:type requester_pays: bool
215+
:param requester_pays:
216+
(Optional) Whether requester pays for API requests for this
217+
bucket and its blobs.
218+
214219
:rtype: :class:`google.cloud.storage.bucket.Bucket`
215220
:returns: The newly created bucket.
216221
"""
217222
bucket = Bucket(self, name=bucket_name)
223+
if requester_pays is not None:
224+
bucket.requester_pays = requester_pays
218225
bucket.create(client=self)
219226
return bucket
220227

storage/tests/system.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
from test_utils.system import unique_resource_id
2929

3030

31+
REQUESTER_PAYS_ENABLED = False # query from environment?
32+
33+
3134
def _bad_copy(bad_request):
3235
"""Predicate: pass only exceptions for a failed copyTo."""
3336
err_msg = bad_request.message
@@ -95,6 +98,15 @@ def test_create_bucket(self):
9598
self.case_buckets_to_delete.append(new_bucket_name)
9699
self.assertEqual(created.name, new_bucket_name)
97100

101+
@unittest.skipUnless(REQUESTER_PAYS_ENABLED, "requesterPays not enabled")
102+
def test_create_bucket_with_requester_pays(self):
103+
new_bucket_name = 'w-requester-pays' + unique_resource_id('-')
104+
created = Config.CLIENT.create_bucket(
105+
new_bucket_name, requester_pays=True)
106+
self.case_buckets_to_delete.append(new_bucket_name)
107+
self.assertEqual(created.name, new_bucket_name)
108+
self.assertTrue(created.requester_pays)
109+
98110
def test_list_buckets(self):
99111
buckets_to_create = [
100112
'new' + unique_resource_id(),

storage/tests/unit/test_bucket.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ def test_create_w_extra_properties(self):
176176
'location': LOCATION,
177177
'storageClass': STORAGE_CLASS,
178178
'versioning': {'enabled': True},
179+
'billing': {'requesterPays': True},
179180
'labels': LABELS,
180181
}
181182
connection = _Connection(DATA)
@@ -186,6 +187,7 @@ def test_create_w_extra_properties(self):
186187
bucket.location = LOCATION
187188
bucket.storage_class = STORAGE_CLASS
188189
bucket.versioning_enabled = True
190+
bucket.requester_pays = True
189191
bucket.labels = LABELS
190192
bucket.create()
191193

@@ -916,6 +918,24 @@ def test_versioning_enabled_setter(self):
916918
bucket.versioning_enabled = True
917919
self.assertTrue(bucket.versioning_enabled)
918920

921+
def test_requester_pays_getter_missing(self):
922+
NAME = 'name'
923+
bucket = self._make_one(name=NAME)
924+
self.assertEqual(bucket.requester_pays, False)
925+
926+
def test_requester_pays_getter(self):
927+
NAME = 'name'
928+
before = {'billing': {'requesterPays': True}}
929+
bucket = self._make_one(name=NAME, properties=before)
930+
self.assertEqual(bucket.requester_pays, True)
931+
932+
def test_requester_pays_setter(self):
933+
NAME = 'name'
934+
bucket = self._make_one(name=NAME)
935+
self.assertFalse(bucket.requester_pays)
936+
bucket.requester_pays = True
937+
self.assertTrue(bucket.requester_pays)
938+
919939
def test_configure_website_defaults(self):
920940
NAME = 'name'
921941
UNSET = {'website': {'mainPageSuffix': None,

storage/tests/unit/test_client.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -184,23 +184,23 @@ def test_get_bucket_hit(self):
184184
CREDENTIALS = _make_credentials()
185185
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)
186186

187-
BLOB_NAME = 'blob-name'
187+
BUCKET_NAME = 'bucket-name'
188188
URI = '/'.join([
189189
client._connection.API_BASE_URL,
190190
'storage',
191191
client._connection.API_VERSION,
192192
'b',
193-
'%s?projection=noAcl' % (BLOB_NAME,),
193+
'%s?projection=noAcl' % (BUCKET_NAME,),
194194
])
195195

196-
data = {'name': BLOB_NAME}
196+
data = {'name': BUCKET_NAME}
197197
http = _make_requests_session([_make_json_response(data)])
198198
client._http_internal = http
199199

200-
bucket = client.get_bucket(BLOB_NAME)
200+
bucket = client.get_bucket(BUCKET_NAME)
201201

202202
self.assertIsInstance(bucket, Bucket)
203-
self.assertEqual(bucket.name, BLOB_NAME)
203+
self.assertEqual(bucket.name, BUCKET_NAME)
204204
http.request.assert_called_once_with(
205205
method='GET', url=URI, data=mock.ANY, headers=mock.ANY)
206206

@@ -234,22 +234,22 @@ def test_lookup_bucket_hit(self):
234234
CREDENTIALS = _make_credentials()
235235
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)
236236

237-
BLOB_NAME = 'blob-name'
237+
BUCKET_NAME = 'bucket-name'
238238
URI = '/'.join([
239239
client._connection.API_BASE_URL,
240240
'storage',
241241
client._connection.API_VERSION,
242242
'b',
243-
'%s?projection=noAcl' % (BLOB_NAME,),
243+
'%s?projection=noAcl' % (BUCKET_NAME,),
244244
])
245-
data = {'name': BLOB_NAME}
245+
data = {'name': BUCKET_NAME}
246246
http = _make_requests_session([_make_json_response(data)])
247247
client._http_internal = http
248248

249-
bucket = client.lookup_bucket(BLOB_NAME)
249+
bucket = client.lookup_bucket(BUCKET_NAME)
250250

251251
self.assertIsInstance(bucket, Bucket)
252-
self.assertEqual(bucket.name, BLOB_NAME)
252+
self.assertEqual(bucket.name, BUCKET_NAME)
253253
http.request.assert_called_once_with(
254254
method='GET', url=URI, data=mock.ANY, headers=mock.ANY)
255255

@@ -260,21 +260,24 @@ def test_create_bucket_conflict(self):
260260
CREDENTIALS = _make_credentials()
261261
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)
262262

263-
BLOB_NAME = 'blob-name'
263+
BUCKET_NAME = 'bucket-name'
264264
URI = '/'.join([
265265
client._connection.API_BASE_URL,
266266
'storage',
267267
client._connection.API_VERSION,
268268
'b?project=%s' % (PROJECT,),
269269
])
270270
data = {'error': {'message': 'Conflict'}}
271+
sent = {'name': BUCKET_NAME}
271272
http = _make_requests_session([
272273
_make_json_response(data, status=http_client.CONFLICT)])
273274
client._http_internal = http
274275

275-
self.assertRaises(Conflict, client.create_bucket, BLOB_NAME)
276+
self.assertRaises(Conflict, client.create_bucket, BUCKET_NAME)
276277
http.request.assert_called_once_with(
277278
method='POST', url=URI, data=mock.ANY, headers=mock.ANY)
279+
json_sent = http.request.call_args_list[0][1]['data']
280+
self.assertEqual(sent, json.loads(json_sent))
278281

279282
def test_create_bucket_success(self):
280283
from google.cloud.storage.bucket import Bucket
@@ -283,23 +286,27 @@ def test_create_bucket_success(self):
283286
CREDENTIALS = _make_credentials()
284287
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)
285288

286-
BLOB_NAME = 'blob-name'
289+
BUCKET_NAME = 'bucket-name'
287290
URI = '/'.join([
288291
client._connection.API_BASE_URL,
289292
'storage',
290293
client._connection.API_VERSION,
291294
'b?project=%s' % (PROJECT,),
292295
])
293-
data = {'name': BLOB_NAME}
296+
sent = {'name': BUCKET_NAME, 'billing': {'requesterPays': True}}
297+
data = sent
294298
http = _make_requests_session([_make_json_response(data)])
295299
client._http_internal = http
296300

297-
bucket = client.create_bucket(BLOB_NAME)
301+
bucket = client.create_bucket(BUCKET_NAME, requester_pays=True)
298302

299303
self.assertIsInstance(bucket, Bucket)
300-
self.assertEqual(bucket.name, BLOB_NAME)
304+
self.assertEqual(bucket.name, BUCKET_NAME)
305+
self.assertTrue(bucket.requester_pays)
301306
http.request.assert_called_once_with(
302307
method='POST', url=URI, data=mock.ANY, headers=mock.ANY)
308+
json_sent = http.request.call_args_list[0][1]['data']
309+
self.assertEqual(sent, json.loads(json_sent))
303310

304311
def test_list_buckets_empty(self):
305312
from six.moves.urllib.parse import parse_qs
@@ -422,7 +429,7 @@ def test_page_non_empty_response(self):
422429
credentials = _make_credentials()
423430
client = self._make_one(project=project, credentials=credentials)
424431

425-
blob_name = 'blob-name'
432+
blob_name = 'bucket-name'
426433
response = {'items': [{'name': blob_name}]}
427434

428435
def dummy_response():

0 commit comments

Comments
 (0)