diff --git a/storage/google/cloud/storage/client.py b/storage/google/cloud/storage/client.py index ac58fb2a092d..9c89b342df4d 100644 --- a/storage/google/cloud/storage/client.py +++ b/storage/google/cloud/storage/client.py @@ -28,6 +28,8 @@ from google.cloud.storage.bucket import Bucket from google.cloud.storage.blob import Blob from google.cloud.storage.hmac_key import HMACKeyMetadata +from google.cloud.storage.acl import BucketACL +from google.cloud.storage.acl import DefaultObjectACL _marker = object() @@ -324,7 +326,16 @@ def lookup_bucket(self, bucket_name): except NotFound: return None - def create_bucket(self, bucket_or_name, requester_pays=None, project=None): + def create_bucket( + self, + bucket_or_name, + requester_pays=None, + project=None, + user_project=None, + location=None, + predefined_acl=None, + predefined_default_object_acl=None, + ): """API call: create a new bucket via a POST request. See @@ -340,8 +351,21 @@ def create_bucket(self, bucket_or_name, requester_pays=None, project=None): Optional. Whether requester pays for API requests for this bucket and its blobs. project (str): - Optional. the project under which the bucket is to be created. + Optional. The project under which the bucket is to be created. If not passed, uses the project set on the client. + user_project (str): + Optional. The project ID to be billed for API requests + made via created bucket. + location (str): + Optional. The location of the bucket. If not passed, + the default location, US, will be used. See + https://cloud.google.com/storage/docs/bucket-locations + predefined_acl (str): + Optional. Name of predefined ACL to apply to bucket. See: + https://cloud.google.com/storage/docs/access-control/lists#predefined-acl + predefined_default_object_acl (str): + Optional. Name of predefined ACL to apply to bucket's objects. See: + https://cloud.google.com/storage/docs/access-control/lists#predefined-acl Returns: google.cloud.storage.bucket.Bucket @@ -374,9 +398,45 @@ def create_bucket(self, bucket_or_name, requester_pays=None, project=None): """ bucket = self._bucket_arg_to_bucket(bucket_or_name) + if project is None: + project = self.project + + if project is None: + raise ValueError("Client project not set: pass an explicit project.") + if requester_pays is not None: bucket.requester_pays = requester_pays - bucket.create(client=self, project=project) + + query_params = {"project": project} + + if predefined_acl is not None: + predefined_acl = BucketACL.validate_predefined(predefined_acl) + query_params["predefinedAcl"] = predefined_acl + + if predefined_default_object_acl is not None: + predefined_default_object_acl = DefaultObjectACL.validate_predefined( + predefined_default_object_acl + ) + query_params["predefinedDefaultObjectAcl"] = predefined_default_object_acl + + if user_project is not None: + query_params["userProject"] = user_project + + properties = {key: bucket._properties[key] for key in bucket._changes} + properties["name"] = bucket.name + + if location is not None: + properties["location"] = location + + api_response = self._connection.api_request( + method="POST", + path="/b", + query_params=query_params, + data=properties, + _target_object=bucket, + ) + + bucket._set_properties(api_response) return bucket def download_blob_to_file(self, blob_or_uri, file_obj, start=None, end=None): diff --git a/storage/tests/unit/test_bucket.py b/storage/tests/unit/test_bucket.py index 2646e05838af..d65ee40f0756 100644 --- a/storage/tests/unit/test_bucket.py +++ b/storage/tests/unit/test_bucket.py @@ -19,6 +19,15 @@ import pytest +def _make_connection(*responses): + import google.cloud.storage._http + + mock_connection = mock.create_autospec(google.cloud.storage._http.Connection) + mock_connection.user_agent = "testing 1.2.3" + mock_connection.api_request.side_effect = list(responses) + return mock_connection + + def _create_signing_credentials(): import google.auth.credentials @@ -599,77 +608,104 @@ def api_request(cls, *args, **kwargs): self.assertEqual(_FakeConnection._called_with, expected_cw) def test_create_w_user_project(self): + from google.cloud.storage.client import Client + PROJECT = "PROJECT" BUCKET_NAME = "bucket-name" USER_PROJECT = "user-project-123" - connection = _Connection() - client = _Client(connection, project=PROJECT) + + client = Client(project=PROJECT) + client._base_connection = _Connection() + bucket = self._make_one(client, BUCKET_NAME, user_project=USER_PROJECT) with self.assertRaises(ValueError): bucket.create() def test_create_w_missing_client_project(self): + from google.cloud.storage.client import Client + BUCKET_NAME = "bucket-name" - connection = _Connection() - client = _Client(connection, project=None) + + client = Client(project=None) bucket = self._make_one(client, BUCKET_NAME) with self.assertRaises(ValueError): bucket.create() def test_create_w_explicit_project(self): + from google.cloud.storage.client import Client + PROJECT = "PROJECT" BUCKET_NAME = "bucket-name" OTHER_PROJECT = "other-project-123" DATA = {"name": BUCKET_NAME} - connection = _Connection(DATA) - client = _Client(connection, project=PROJECT) - bucket = self._make_one(client, BUCKET_NAME) + connection = _make_connection(DATA) - bucket.create(project=OTHER_PROJECT) + client = Client(project=PROJECT) + client._base_connection = connection - kw, = connection._requested - self.assertEqual(kw["method"], "POST") - self.assertEqual(kw["path"], "/b") - self.assertEqual(kw["query_params"], {"project": OTHER_PROJECT}) - self.assertEqual(kw["data"], DATA) + bucket = self._make_one(client, BUCKET_NAME) + bucket.create(project=OTHER_PROJECT) + connection.api_request.assert_called_once_with( + method="POST", + path="/b", + query_params={"project": OTHER_PROJECT}, + data=DATA, + _target_object=bucket, + ) def test_create_w_explicit_location(self): + from google.cloud.storage.client import Client + PROJECT = "PROJECT" BUCKET_NAME = "bucket-name" LOCATION = "us-central1" DATA = {"location": LOCATION, "name": BUCKET_NAME} - connection = _Connection( + + connection = _make_connection( DATA, "{'location': 'us-central1', 'name': 'bucket-name'}" ) - client = _Client(connection, project=PROJECT) - bucket = self._make_one(client, BUCKET_NAME) + client = Client(project=PROJECT) + client._base_connection = connection + + bucket = self._make_one(client, BUCKET_NAME) bucket.create(location=LOCATION) - kw, = connection._requested - self.assertEqual(kw["method"], "POST") - self.assertEqual(kw["path"], "/b") - self.assertEqual(kw["data"], DATA) + connection.api_request.assert_called_once_with( + method="POST", + path="/b", + data=DATA, + _target_object=bucket, + query_params={"project": "PROJECT"}, + ) self.assertEqual(bucket.location, LOCATION) def test_create_hit(self): + from google.cloud.storage.client import Client + PROJECT = "PROJECT" BUCKET_NAME = "bucket-name" DATA = {"name": BUCKET_NAME} - connection = _Connection(DATA) - client = _Client(connection, project=PROJECT) + connection = _make_connection(DATA) + client = Client(project=PROJECT) + client._base_connection = connection + bucket = self._make_one(client=client, name=BUCKET_NAME) bucket.create() - kw, = connection._requested - self.assertEqual(kw["method"], "POST") - self.assertEqual(kw["path"], "/b") - self.assertEqual(kw["query_params"], {"project": PROJECT}) - self.assertEqual(kw["data"], DATA) + connection.api_request.assert_called_once_with( + method="POST", + path="/b", + query_params={"project": PROJECT}, + data=DATA, + _target_object=bucket, + ) def test_create_w_extra_properties(self): + from google.cloud.storage.client import Client + BUCKET_NAME = "bucket-name" PROJECT = "PROJECT" CORS = [ @@ -694,8 +730,11 @@ def test_create_w_extra_properties(self): "billing": {"requesterPays": True}, "labels": LABELS, } - connection = _Connection(DATA) - client = _Client(connection, project=PROJECT) + + connection = _make_connection(DATA) + client = Client(project=PROJECT) + client._base_connection = connection + bucket = self._make_one(client=client, name=BUCKET_NAME) bucket.cors = CORS bucket.lifecycle_rules = LIFECYCLE_RULES @@ -705,29 +744,37 @@ def test_create_w_extra_properties(self): bucket.labels = LABELS bucket.create(location=LOCATION) - kw, = connection._requested - self.assertEqual(kw["method"], "POST") - self.assertEqual(kw["path"], "/b") - self.assertEqual(kw["query_params"], {"project": PROJECT}) - self.assertEqual(kw["data"], DATA) + connection.api_request.assert_called_once_with( + method="POST", + path="/b", + query_params={"project": PROJECT}, + data=DATA, + _target_object=bucket, + ) def test_create_w_predefined_acl_invalid(self): + from google.cloud.storage.client import Client + PROJECT = "PROJECT" BUCKET_NAME = "bucket-name" DATA = {"name": BUCKET_NAME} connection = _Connection(DATA) - client = _Client(connection, project=PROJECT) + client = Client(project=PROJECT) + client._base_connection = connection bucket = self._make_one(client=client, name=BUCKET_NAME) with self.assertRaises(ValueError): bucket.create(predefined_acl="bogus") def test_create_w_predefined_acl_valid(self): + from google.cloud.storage.client import Client + PROJECT = "PROJECT" BUCKET_NAME = "bucket-name" DATA = {"name": BUCKET_NAME} connection = _Connection(DATA) - client = _Client(connection, project=PROJECT) + client = Client(project=PROJECT) + client._base_connection = connection bucket = self._make_one(client=client, name=BUCKET_NAME) bucket.create(predefined_acl="publicRead") @@ -739,22 +786,28 @@ def test_create_w_predefined_acl_valid(self): self.assertEqual(kw["data"], DATA) def test_create_w_predefined_default_object_acl_invalid(self): + from google.cloud.storage.client import Client + PROJECT = "PROJECT" BUCKET_NAME = "bucket-name" DATA = {"name": BUCKET_NAME} connection = _Connection(DATA) - client = _Client(connection, project=PROJECT) + client = Client(project=PROJECT) + client._base_connection = connection bucket = self._make_one(client=client, name=BUCKET_NAME) with self.assertRaises(ValueError): bucket.create(predefined_default_object_acl="bogus") def test_create_w_predefined_default_object_acl_valid(self): + from google.cloud.storage.client import Client + PROJECT = "PROJECT" BUCKET_NAME = "bucket-name" DATA = {"name": BUCKET_NAME} connection = _Connection(DATA) - client = _Client(connection, project=PROJECT) + client = Client(project=PROJECT) + client._base_connection = connection bucket = self._make_one(client=client, name=BUCKET_NAME) bucket.create(predefined_default_object_acl="publicRead") diff --git a/storage/tests/unit/test_client.py b/storage/tests/unit/test_client.py index f8a857d164a0..5141ba6e83d0 100644 --- a/storage/tests/unit/test_client.py +++ b/storage/tests/unit/test_client.py @@ -521,6 +521,7 @@ def test_create_bucket_with_string_conflict(self): from google.cloud.exceptions import Conflict project = "PROJECT" + user_project = "USER_PROJECT" other_project = "OTHER_PROJECT" credentials = _make_credentials() client = self._make_one(project=project, credentials=credentials) @@ -531,7 +532,7 @@ def test_create_bucket_with_string_conflict(self): client._connection.API_BASE_URL, "storage", client._connection.API_VERSION, - "b?project=%s" % (other_project,), + "b?project=%s&userProject=%s" % (other_project, user_project), ] ) data = {"error": {"message": "Conflict"}} @@ -542,7 +543,9 @@ def test_create_bucket_with_string_conflict(self): client._http_internal = http with self.assertRaises(Conflict): - client.create_bucket(bucket_name, project=other_project) + client.create_bucket( + bucket_name, project=other_project, user_project=user_project + ) http.request.assert_called_once_with( method="POST", url=URI, data=mock.ANY, headers=mock.ANY @@ -585,6 +588,102 @@ def test_create_bucket_with_object_conflict(self): json_sent = http.request.call_args_list[0][1]["data"] self.assertEqual(json_expected, json.loads(json_sent)) + def test_create_w_missing_client_project(self): + client = self._make_one(project=None) + + with self.assertRaises(ValueError): + client.create_bucket("bucket") + + def test_create_w_predefined_acl_invalid(self): + PROJECT = "PROJECT" + BUCKET_NAME = "bucket-name" + credentials = _make_credentials() + client = self._make_one(project=PROJECT, credentials=credentials) + + with self.assertRaises(ValueError): + client.create_bucket(BUCKET_NAME, predefined_acl="bogus") + + def test_create_w_predefined_acl_valid(self): + from google.cloud.storage.client import Client + + PROJECT = "PROJECT" + BUCKET_NAME = "bucket-name" + DATA = {"name": BUCKET_NAME} + + client = Client(project=PROJECT) + connection = _make_connection(DATA) + client._base_connection = connection + bucket = client.create_bucket(BUCKET_NAME, predefined_acl="publicRead") + + connection.api_request.assert_called_once_with( + method="POST", + path="/b", + query_params={"project": PROJECT, "predefinedAcl": "publicRead"}, + data=DATA, + _target_object=bucket, + ) + + def test_create_w_predefined_default_object_acl_invalid(self): + PROJECT = "PROJECT" + BUCKET_NAME = "bucket-name" + + credentials = _make_credentials() + client = self._make_one(project=PROJECT, credentials=credentials) + + with self.assertRaises(ValueError): + client.create_bucket(BUCKET_NAME, predefined_default_object_acl="bogus") + + def test_create_w_predefined_default_object_acl_valid(self): + from google.cloud.storage.client import Client + + PROJECT = "PROJECT" + BUCKET_NAME = "bucket-name" + DATA = {"name": BUCKET_NAME} + + client = Client(project=PROJECT) + connection = _make_connection(DATA) + client._base_connection = connection + bucket = client.create_bucket( + BUCKET_NAME, predefined_default_object_acl="publicRead" + ) + + connection.api_request.assert_called_once_with( + method="POST", + path="/b", + query_params={ + "project": PROJECT, + "predefinedDefaultObjectAcl": "publicRead", + }, + data=DATA, + _target_object=bucket, + ) + + def test_create_w_explicit_location(self): + from google.cloud.storage.client import Client + + PROJECT = "PROJECT" + BUCKET_NAME = "bucket-name" + LOCATION = "us-central1" + DATA = {"location": LOCATION, "name": BUCKET_NAME} + + connection = _make_connection( + DATA, "{'location': 'us-central1', 'name': 'bucket-name'}" + ) + + client = Client(project=PROJECT) + client._base_connection = connection + + bucket = client.create_bucket(BUCKET_NAME, location=LOCATION) + + connection.api_request.assert_called_once_with( + method="POST", + path="/b", + data=DATA, + _target_object=bucket, + query_params={"project": "PROJECT"}, + ) + self.assertEqual(bucket.location, LOCATION) + def test_create_bucket_with_string_success(self): from google.cloud.storage.bucket import Bucket