Skip to content

Commit 0c254f2

Browse files
tseaverlandrito
authored andcommitted
Add 'Bucket.requester_pays' property. (googleapis#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 googleapis#3474.
1 parent 4954cd7 commit 0c254f2

5 files changed

Lines changed: 94 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
@@ -798,10 +798,40 @@ def versioning_enabled(self, value):
798798
details.
799799
800800
:type value: convertible to boolean
801-
:param value: should versioning be anabled for the bucket?
801+
:param value: should versioning be enabled for the bucket?
802802
"""
803803
self._patch_property('versioning', {'enabled': bool(value)})
804804

805+
@property
806+
def requester_pays(self):
807+
"""Does the requester pay for API requests for this bucket?
808+
809+
.. note::
810+
811+
No public docs exist yet for the "requester pays" feature.
812+
813+
:setter: Update whether requester pays for this bucket.
814+
:getter: Query whether requester pays for this bucket.
815+
816+
:rtype: bool
817+
:returns: True if requester pays for API requests for the bucket,
818+
else False.
819+
"""
820+
versioning = self._properties.get('billing', {})
821+
return versioning.get('requesterPays', False)
822+
823+
@requester_pays.setter
824+
def requester_pays(self, value):
825+
"""Update whether requester pays for API requests for this bucket.
826+
827+
See https://cloud.google.com/storage/docs/<DOCS-MISSING> for
828+
details.
829+
830+
:type value: convertible to boolean
831+
:param value: should requester pay for API requests for the bucket?
832+
"""
833+
self._patch_property('billing', {'requesterPays': bool(value)})
834+
805835
def configure_website(self, main_page_suffix=None, not_found_page=None):
806836
"""Configure website-related properties.
807837

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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030

3131
HTTP = httplib2.Http()
3232

33+
REQUESTER_PAYS_ENABLED = False # query from environment?
34+
3335

3436
def _bad_copy(bad_request):
3537
"""Predicate: pass only exceptions for a failed copyTo."""
@@ -99,6 +101,15 @@ def test_create_bucket(self):
99101
self.case_buckets_to_delete.append(new_bucket_name)
100102
self.assertEqual(created.name, new_bucket_name)
101103

104+
@unittest.skipUnless(REQUESTER_PAYS_ENABLED, "requesterPays not enabled")
105+
def test_create_bucket_with_requester_pays(self):
106+
new_bucket_name = 'w-requester-pays' + unique_resource_id('-')
107+
created = Config.CLIENT.create_bucket(
108+
new_bucket_name, requester_pays=True)
109+
self.case_buckets_to_delete.append(new_bucket_name)
110+
self.assertEqual(created.name, new_bucket_name)
111+
self.assertTrue(created.requester_pays)
112+
102113
def test_list_buckets(self):
103114
buckets_to_create = [
104115
'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

@@ -866,6 +868,24 @@ def test_versioning_enabled_setter(self):
866868
bucket.versioning_enabled = True
867869
self.assertTrue(bucket.versioning_enabled)
868870

871+
def test_requester_pays_getter_missing(self):
872+
NAME = 'name'
873+
bucket = self._make_one(name=NAME)
874+
self.assertEqual(bucket.requester_pays, False)
875+
876+
def test_requester_pays_getter(self):
877+
NAME = 'name'
878+
before = {'billing': {'requesterPays': True}}
879+
bucket = self._make_one(name=NAME, properties=before)
880+
self.assertEqual(bucket.requester_pays, True)
881+
882+
def test_requester_pays_setter(self):
883+
NAME = 'name'
884+
bucket = self._make_one(name=NAME)
885+
self.assertFalse(bucket.requester_pays)
886+
bucket.requester_pays = True
887+
self.assertTrue(bucket.requester_pays)
888+
869889
def test_configure_website_defaults(self):
870890
NAME = 'name'
871891
UNSET = {'website': {'mainPageSuffix': None,

storage/tests/unit/test_client.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -155,22 +155,22 @@ def test_get_bucket_hit(self):
155155
CREDENTIALS = _make_credentials()
156156
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)
157157

158-
BLOB_NAME = 'blob-name'
158+
BUCKET_NAME = 'bucket-name'
159159
URI = '/'.join([
160160
client._connection.API_BASE_URL,
161161
'storage',
162162
client._connection.API_VERSION,
163163
'b',
164-
'%s?projection=noAcl' % (BLOB_NAME,),
164+
'%s?projection=noAcl' % (BUCKET_NAME,),
165165
])
166166
http = client._http_internal = _Http(
167167
{'status': '200', 'content-type': 'application/json'},
168-
'{{"name": "{0}"}}'.format(BLOB_NAME).encode('utf-8'),
168+
'{{"name": "{0}"}}'.format(BUCKET_NAME).encode('utf-8'),
169169
)
170170

171-
bucket = client.get_bucket(BLOB_NAME)
171+
bucket = client.get_bucket(BUCKET_NAME)
172172
self.assertIsInstance(bucket, Bucket)
173-
self.assertEqual(bucket.name, BLOB_NAME)
173+
self.assertEqual(bucket.name, BUCKET_NAME)
174174
self.assertEqual(http._called_with['method'], 'GET')
175175
self.assertEqual(http._called_with['uri'], URI)
176176

@@ -203,33 +203,34 @@ def test_lookup_bucket_hit(self):
203203
CREDENTIALS = _make_credentials()
204204
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)
205205

206-
BLOB_NAME = 'blob-name'
206+
BUCKET_NAME = 'bucket-name'
207207
URI = '/'.join([
208208
client._connection.API_BASE_URL,
209209
'storage',
210210
client._connection.API_VERSION,
211211
'b',
212-
'%s?projection=noAcl' % (BLOB_NAME,),
212+
'%s?projection=noAcl' % (BUCKET_NAME,),
213213
])
214214
http = client._http_internal = _Http(
215215
{'status': '200', 'content-type': 'application/json'},
216-
'{{"name": "{0}"}}'.format(BLOB_NAME).encode('utf-8'),
216+
'{{"name": "{0}"}}'.format(BUCKET_NAME).encode('utf-8'),
217217
)
218218

219-
bucket = client.lookup_bucket(BLOB_NAME)
219+
bucket = client.lookup_bucket(BUCKET_NAME)
220220
self.assertIsInstance(bucket, Bucket)
221-
self.assertEqual(bucket.name, BLOB_NAME)
221+
self.assertEqual(bucket.name, BUCKET_NAME)
222222
self.assertEqual(http._called_with['method'], 'GET')
223223
self.assertEqual(http._called_with['uri'], URI)
224224

225225
def test_create_bucket_conflict(self):
226+
import json
226227
from google.cloud.exceptions import Conflict
227228

228229
PROJECT = 'PROJECT'
229230
CREDENTIALS = _make_credentials()
230231
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)
231232

232-
BLOB_NAME = 'blob-name'
233+
BUCKET_NAME = 'bucket-name'
233234
URI = '/'.join([
234235
client._connection.API_BASE_URL,
235236
'storage',
@@ -241,18 +242,21 @@ def test_create_bucket_conflict(self):
241242
'{"error": {"message": "Conflict"}}',
242243
)
243244

244-
self.assertRaises(Conflict, client.create_bucket, BLOB_NAME)
245+
self.assertRaises(Conflict, client.create_bucket, BUCKET_NAME)
245246
self.assertEqual(http._called_with['method'], 'POST')
246247
self.assertEqual(http._called_with['uri'], URI)
248+
body = json.loads(http._called_with['body'])
249+
self.assertEqual(body, {'name': BUCKET_NAME})
247250

248251
def test_create_bucket_success(self):
252+
import json
249253
from google.cloud.storage.bucket import Bucket
250254

251255
PROJECT = 'PROJECT'
252256
CREDENTIALS = _make_credentials()
253257
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)
254258

255-
BLOB_NAME = 'blob-name'
259+
BUCKET_NAME = 'bucket-name'
256260
URI = '/'.join([
257261
client._connection.API_BASE_URL,
258262
'storage',
@@ -261,14 +265,17 @@ def test_create_bucket_success(self):
261265
])
262266
http = client._http_internal = _Http(
263267
{'status': '200', 'content-type': 'application/json'},
264-
'{{"name": "{0}"}}'.format(BLOB_NAME).encode('utf-8'),
268+
'{{"name": "{0}"}}'.format(BUCKET_NAME).encode('utf-8'),
265269
)
266270

267-
bucket = client.create_bucket(BLOB_NAME)
271+
bucket = client.create_bucket(BUCKET_NAME, requester_pays=True)
268272
self.assertIsInstance(bucket, Bucket)
269-
self.assertEqual(bucket.name, BLOB_NAME)
273+
self.assertEqual(bucket.name, BUCKET_NAME)
270274
self.assertEqual(http._called_with['method'], 'POST')
271275
self.assertEqual(http._called_with['uri'], URI)
276+
body = json.loads(http._called_with['body'])
277+
self.assertEqual(
278+
body, {'name': BUCKET_NAME, 'billing': {'requesterPays': True}})
272279

273280
def test_list_buckets_empty(self):
274281
from six.moves.urllib.parse import parse_qs
@@ -400,7 +407,7 @@ def test_page_non_empty_response(self):
400407
credentials = _make_credentials()
401408
client = self._make_one(project=project, credentials=credentials)
402409

403-
blob_name = 'blob-name'
410+
blob_name = 'bucket-name'
404411
response = {'items': [{'name': blob_name}]}
405412

406413
def dummy_response():

0 commit comments

Comments
 (0)