Skip to content

Commit

Permalink
Add 'Bucket.requester_pays' property. (googleapis#3488)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tseaver authored and landrito committed Aug 21, 2017
1 parent 655f23c commit d1276d6
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 19 deletions.
32 changes: 31 additions & 1 deletion storage/google/cloud/storage/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,10 +798,40 @@ def versioning_enabled(self, value):
details.
:type value: convertible to boolean
:param value: should versioning be anabled for the bucket?
:param value: should versioning be enabled for the bucket?
"""
self._patch_property('versioning', {'enabled': bool(value)})

@property
def requester_pays(self):
"""Does the requester pay for API requests for this bucket?
.. note::
No public docs exist yet for the "requester pays" feature.
:setter: Update whether requester pays for this bucket.
:getter: Query whether requester pays for this bucket.
:rtype: bool
:returns: True if requester pays for API requests for the bucket,
else False.
"""
versioning = self._properties.get('billing', {})
return versioning.get('requesterPays', False)

@requester_pays.setter
def requester_pays(self, value):
"""Update whether requester pays for API requests for this bucket.
See https://cloud.google.com/storage/docs/<DOCS-MISSING> for
details.
:type value: convertible to boolean
:param value: should requester pay for API requests for the bucket?
"""
self._patch_property('billing', {'requesterPays': bool(value)})

def configure_website(self, main_page_suffix=None, not_found_page=None):
"""Configure website-related properties.
Expand Down
9 changes: 8 additions & 1 deletion storage/google/cloud/storage/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def lookup_bucket(self, bucket_name):
except NotFound:
return None

def create_bucket(self, bucket_name):
def create_bucket(self, bucket_name, requester_pays=None):
"""Create a new bucket.
For example:
Expand All @@ -211,10 +211,17 @@ def create_bucket(self, bucket_name):
:type bucket_name: str
:param bucket_name: The bucket name to create.
:type requester_pays: bool
:param requester_pays:
(Optional) Whether requester pays for API requests for this
bucket and its blobs.
:rtype: :class:`google.cloud.storage.bucket.Bucket`
:returns: The newly created bucket.
"""
bucket = Bucket(self, name=bucket_name)
if requester_pays is not None:
bucket.requester_pays = requester_pays
bucket.create(client=self)
return bucket

Expand Down
11 changes: 11 additions & 0 deletions storage/tests/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

HTTP = httplib2.Http()

REQUESTER_PAYS_ENABLED = False # query from environment?


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

@unittest.skipUnless(REQUESTER_PAYS_ENABLED, "requesterPays not enabled")
def test_create_bucket_with_requester_pays(self):
new_bucket_name = 'w-requester-pays' + unique_resource_id('-')
created = Config.CLIENT.create_bucket(
new_bucket_name, requester_pays=True)
self.case_buckets_to_delete.append(new_bucket_name)
self.assertEqual(created.name, new_bucket_name)
self.assertTrue(created.requester_pays)

def test_list_buckets(self):
buckets_to_create = [
'new' + unique_resource_id(),
Expand Down
20 changes: 20 additions & 0 deletions storage/tests/unit/test_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ def test_create_w_extra_properties(self):
'location': LOCATION,
'storageClass': STORAGE_CLASS,
'versioning': {'enabled': True},
'billing': {'requesterPays': True},
'labels': LABELS,
}
connection = _Connection(DATA)
Expand All @@ -186,6 +187,7 @@ def test_create_w_extra_properties(self):
bucket.location = LOCATION
bucket.storage_class = STORAGE_CLASS
bucket.versioning_enabled = True
bucket.requester_pays = True
bucket.labels = LABELS
bucket.create()

Expand Down Expand Up @@ -866,6 +868,24 @@ def test_versioning_enabled_setter(self):
bucket.versioning_enabled = True
self.assertTrue(bucket.versioning_enabled)

def test_requester_pays_getter_missing(self):
NAME = 'name'
bucket = self._make_one(name=NAME)
self.assertEqual(bucket.requester_pays, False)

def test_requester_pays_getter(self):
NAME = 'name'
before = {'billing': {'requesterPays': True}}
bucket = self._make_one(name=NAME, properties=before)
self.assertEqual(bucket.requester_pays, True)

def test_requester_pays_setter(self):
NAME = 'name'
bucket = self._make_one(name=NAME)
self.assertFalse(bucket.requester_pays)
bucket.requester_pays = True
self.assertTrue(bucket.requester_pays)

def test_configure_website_defaults(self):
NAME = 'name'
UNSET = {'website': {'mainPageSuffix': None,
Expand Down
41 changes: 24 additions & 17 deletions storage/tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,22 +155,22 @@ def test_get_bucket_hit(self):
CREDENTIALS = _make_credentials()
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)

BLOB_NAME = 'blob-name'
BUCKET_NAME = 'bucket-name'
URI = '/'.join([
client._connection.API_BASE_URL,
'storage',
client._connection.API_VERSION,
'b',
'%s?projection=noAcl' % (BLOB_NAME,),
'%s?projection=noAcl' % (BUCKET_NAME,),
])
http = client._http_internal = _Http(
{'status': '200', 'content-type': 'application/json'},
'{{"name": "{0}"}}'.format(BLOB_NAME).encode('utf-8'),
'{{"name": "{0}"}}'.format(BUCKET_NAME).encode('utf-8'),
)

bucket = client.get_bucket(BLOB_NAME)
bucket = client.get_bucket(BUCKET_NAME)
self.assertIsInstance(bucket, Bucket)
self.assertEqual(bucket.name, BLOB_NAME)
self.assertEqual(bucket.name, BUCKET_NAME)
self.assertEqual(http._called_with['method'], 'GET')
self.assertEqual(http._called_with['uri'], URI)

Expand Down Expand Up @@ -203,33 +203,34 @@ def test_lookup_bucket_hit(self):
CREDENTIALS = _make_credentials()
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)

BLOB_NAME = 'blob-name'
BUCKET_NAME = 'bucket-name'
URI = '/'.join([
client._connection.API_BASE_URL,
'storage',
client._connection.API_VERSION,
'b',
'%s?projection=noAcl' % (BLOB_NAME,),
'%s?projection=noAcl' % (BUCKET_NAME,),
])
http = client._http_internal = _Http(
{'status': '200', 'content-type': 'application/json'},
'{{"name": "{0}"}}'.format(BLOB_NAME).encode('utf-8'),
'{{"name": "{0}"}}'.format(BUCKET_NAME).encode('utf-8'),
)

bucket = client.lookup_bucket(BLOB_NAME)
bucket = client.lookup_bucket(BUCKET_NAME)
self.assertIsInstance(bucket, Bucket)
self.assertEqual(bucket.name, BLOB_NAME)
self.assertEqual(bucket.name, BUCKET_NAME)
self.assertEqual(http._called_with['method'], 'GET')
self.assertEqual(http._called_with['uri'], URI)

def test_create_bucket_conflict(self):
import json
from google.cloud.exceptions import Conflict

PROJECT = 'PROJECT'
CREDENTIALS = _make_credentials()
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)

BLOB_NAME = 'blob-name'
BUCKET_NAME = 'bucket-name'
URI = '/'.join([
client._connection.API_BASE_URL,
'storage',
Expand All @@ -241,18 +242,21 @@ def test_create_bucket_conflict(self):
'{"error": {"message": "Conflict"}}',
)

self.assertRaises(Conflict, client.create_bucket, BLOB_NAME)
self.assertRaises(Conflict, client.create_bucket, BUCKET_NAME)
self.assertEqual(http._called_with['method'], 'POST')
self.assertEqual(http._called_with['uri'], URI)
body = json.loads(http._called_with['body'])
self.assertEqual(body, {'name': BUCKET_NAME})

def test_create_bucket_success(self):
import json
from google.cloud.storage.bucket import Bucket

PROJECT = 'PROJECT'
CREDENTIALS = _make_credentials()
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)

BLOB_NAME = 'blob-name'
BUCKET_NAME = 'bucket-name'
URI = '/'.join([
client._connection.API_BASE_URL,
'storage',
Expand All @@ -261,14 +265,17 @@ def test_create_bucket_success(self):
])
http = client._http_internal = _Http(
{'status': '200', 'content-type': 'application/json'},
'{{"name": "{0}"}}'.format(BLOB_NAME).encode('utf-8'),
'{{"name": "{0}"}}'.format(BUCKET_NAME).encode('utf-8'),
)

bucket = client.create_bucket(BLOB_NAME)
bucket = client.create_bucket(BUCKET_NAME, requester_pays=True)
self.assertIsInstance(bucket, Bucket)
self.assertEqual(bucket.name, BLOB_NAME)
self.assertEqual(bucket.name, BUCKET_NAME)
self.assertEqual(http._called_with['method'], 'POST')
self.assertEqual(http._called_with['uri'], URI)
body = json.loads(http._called_with['body'])
self.assertEqual(
body, {'name': BUCKET_NAME, 'billing': {'requesterPays': True}})

def test_list_buckets_empty(self):
from six.moves.urllib.parse import parse_qs
Expand Down Expand Up @@ -400,7 +407,7 @@ def test_page_non_empty_response(self):
credentials = _make_credentials()
client = self._make_one(project=project, credentials=credentials)

blob_name = 'blob-name'
blob_name = 'bucket-name'
response = {'items': [{'name': blob_name}]}

def dummy_response():
Expand Down

0 comments on commit d1276d6

Please sign in to comment.