Skip to content

Commit edade4c

Browse files
authored
Make 'project' optional / overridable for storage client (googleapis#4381)
* Honor explicit 'project=None' for client. * Add explicit 'project' param to'list_buckets'. * Add explicit 'project' param to 'Bucket.create' / 'Client.create_buck… * Enforce that 'topic_project' is passed if 'Client.project' is None. Closes googleapis#4239
1 parent ffba840 commit edade4c

File tree

6 files changed

+172
-10
lines changed

6 files changed

+172
-10
lines changed

storage/google/cloud/storage/bucket.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ def exists(self, client=None):
251251
except NotFound:
252252
return False
253253

254-
def create(self, client=None):
254+
def create(self, client=None, project=None):
255255
"""Creates current bucket.
256256
257257
If the bucket already exists, will raise
@@ -265,12 +265,28 @@ def create(self, client=None):
265265
``NoneType``
266266
:param client: Optional. The client to use. If not passed, falls back
267267
to the ``client`` stored on the current bucket.
268+
269+
:type project: str
270+
:param project: (Optional) the project under which the bucket is to
271+
be created. If not passed, uses the project set on
272+
the client.
273+
:raises ValueError: if :attr:`user_project` is set.
274+
:raises ValueError: if ``project`` is None and client's
275+
:attr:`project` is also None.
268276
"""
269277
if self.user_project is not None:
270278
raise ValueError("Cannot create bucket with 'user_project' set.")
271279

272280
client = self._require_client(client)
273-
query_params = {'project': client.project}
281+
282+
if project is None:
283+
project = client.project
284+
285+
if project is None:
286+
raise ValueError(
287+
"Client project not set: pass an explicit project.")
288+
289+
query_params = {'project': project}
274290
properties = {key: self._properties[key] for key in self._changes}
275291
properties['name'] = self.name
276292
api_response = client._connection.api_request(

storage/google/cloud/storage/client.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@
2626
from google.cloud.storage.bucket import Bucket
2727

2828

29+
_marker = object()
30+
31+
2932
class Client(ClientWithProject):
3033
"""Client to bundle configuration needed for API requests.
3134
32-
:type project: str
35+
:type project: str or None
3336
:param project: the project which the client acts on behalf of. Will be
3437
passed when creating a topic. If not passed,
3538
falls back to the default inferred from the environment.
@@ -55,10 +58,19 @@ class Client(ClientWithProject):
5558
'https://www.googleapis.com/auth/devstorage.read_write')
5659
"""The scopes required for authenticating as a Cloud Storage consumer."""
5760

58-
def __init__(self, project=None, credentials=None, _http=None):
61+
def __init__(self, project=_marker, credentials=None, _http=None):
5962
self._base_connection = None
63+
if project is None:
64+
no_project = True
65+
project = '<none>'
66+
else:
67+
no_project = False
68+
if project is _marker:
69+
project = None
6070
super(Client, self).__init__(project=project, credentials=credentials,
6171
_http=_http)
72+
if no_project:
73+
self.project = None
6274
self._connection = Connection(self)
6375
self._batch_stack = _LocalStack()
6476

@@ -216,7 +228,7 @@ def lookup_bucket(self, bucket_name):
216228
except NotFound:
217229
return None
218230

219-
def create_bucket(self, bucket_name, requester_pays=None):
231+
def create_bucket(self, bucket_name, requester_pays=None, project=None):
220232
"""Create a new bucket.
221233
222234
For example:
@@ -238,17 +250,22 @@ def create_bucket(self, bucket_name, requester_pays=None):
238250
(Optional) Whether requester pays for API requests for this
239251
bucket and its blobs.
240252
253+
:type project: str
254+
:param project: (Optional) the project under which the bucket is to
255+
be created. If not passed, uses the project set on
256+
the client.
257+
241258
:rtype: :class:`google.cloud.storage.bucket.Bucket`
242259
:returns: The newly created bucket.
243260
"""
244261
bucket = Bucket(self, name=bucket_name)
245262
if requester_pays is not None:
246263
bucket.requester_pays = requester_pays
247-
bucket.create(client=self)
264+
bucket.create(client=self, project=project)
248265
return bucket
249266

250267
def list_buckets(self, max_results=None, page_token=None, prefix=None,
251-
projection='noAcl', fields=None):
268+
projection='noAcl', fields=None, project=None):
252269
"""Get all buckets in the project associated to the client.
253270
254271
This will not populate the list of blobs available in each
@@ -284,11 +301,24 @@ def list_buckets(self, max_results=None, page_token=None, prefix=None,
284301
response with just the next page token and the language of each
285302
bucket returned: 'items/id,nextPageToken'
286303
304+
:type project: str
305+
:param project: (Optional) the project whose buckets are to be listed.
306+
If not passed, uses the project set on the client.
307+
287308
:rtype: :class:`~google.api_core.page_iterator.Iterator`
309+
:raises ValueError: if both ``project`` is ``None`` and the client's
310+
project is also ``None``.
288311
:returns: Iterator of all :class:`~google.cloud.storage.bucket.Bucket`
289312
belonging to this project.
290313
"""
291-
extra_params = {'project': self.project}
314+
if project is None:
315+
project = self.project
316+
317+
if project is None:
318+
raise ValueError(
319+
"Client project not set: pass an explicit project.")
320+
321+
extra_params = {'project': project}
292322

293323
if prefix is not None:
294324
extra_params['prefix'] = prefix

storage/google/cloud/storage/notification.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ def __init__(self, bucket, topic_name,
7676

7777
if topic_project is None:
7878
topic_project = bucket.client.project
79+
80+
if topic_project is None:
81+
raise ValueError(
82+
"Client project not set: pass an explicit topic_project.")
83+
7984
self._topic_project = topic_project
8085

8186
self._properties = {}

storage/tests/unit/test_bucket.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,33 @@ def test_create_w_user_project(self):
239239
with self.assertRaises(ValueError):
240240
bucket.create()
241241

242+
def test_create_w_missing_client_project(self):
243+
BUCKET_NAME = 'bucket-name'
244+
USER_PROJECT = 'user-project-123'
245+
connection = _Connection()
246+
client = _Client(connection, project=None)
247+
bucket = self._make_one(client, BUCKET_NAME)
248+
249+
with self.assertRaises(ValueError):
250+
bucket.create()
251+
252+
def test_create_w_explicit_project(self):
253+
PROJECT = 'PROJECT'
254+
BUCKET_NAME = 'bucket-name'
255+
OTHER_PROJECT = 'other-project-123'
256+
DATA = {'name': BUCKET_NAME}
257+
connection = _Connection(DATA)
258+
client = _Client(connection, project=PROJECT)
259+
bucket = self._make_one(client, BUCKET_NAME)
260+
261+
bucket.create(project=OTHER_PROJECT)
262+
263+
kw, = connection._requested
264+
self.assertEqual(kw['method'], 'POST')
265+
self.assertEqual(kw['path'], '/b')
266+
self.assertEqual(kw['query_params'], {'project': OTHER_PROJECT})
267+
self.assertEqual(kw['data'], DATA)
268+
242269
def test_create_hit(self):
243270
PROJECT = 'PROJECT'
244271
BUCKET_NAME = 'bucket-name'

storage/tests/unit/test_client.py

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,38 @@ def test_ctor_connection_type(self):
7575
self.assertIsNone(client.current_batch)
7676
self.assertEqual(list(client._batch_stack), [])
7777

78+
def test_ctor_wo_project(self):
79+
from google.cloud.storage._http import Connection
80+
81+
PROJECT = 'PROJECT'
82+
CREDENTIALS = _make_credentials()
83+
84+
ddp_patch = mock.patch(
85+
'google.cloud.client._determine_default_project',
86+
return_value=PROJECT)
87+
88+
with ddp_patch:
89+
client = self._make_one(credentials=CREDENTIALS)
90+
91+
self.assertEqual(client.project, PROJECT)
92+
self.assertIsInstance(client._connection, Connection)
93+
self.assertIs(client._connection.credentials, CREDENTIALS)
94+
self.assertIsNone(client.current_batch)
95+
self.assertEqual(list(client._batch_stack), [])
96+
97+
def test_ctor_w_project_explicit_none(self):
98+
from google.cloud.storage._http import Connection
99+
100+
CREDENTIALS = _make_credentials()
101+
102+
client = self._make_one(project=None, credentials=CREDENTIALS)
103+
104+
self.assertIsNone(client.project)
105+
self.assertIsInstance(client._connection, Connection)
106+
self.assertIs(client._connection.credentials, CREDENTIALS)
107+
self.assertIsNone(client.current_batch)
108+
self.assertEqual(list(client._batch_stack), [])
109+
78110
def test_create_anonymous_client(self):
79111
from google.auth.credentials import AnonymousCredentials
80112
from google.cloud.storage._http import Connection
@@ -286,6 +318,7 @@ def test_create_bucket_conflict(self):
286318
from google.cloud.exceptions import Conflict
287319

288320
PROJECT = 'PROJECT'
321+
OTHER_PROJECT = 'OTHER_PROJECT'
289322
CREDENTIALS = _make_credentials()
290323
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)
291324

@@ -294,15 +327,17 @@ def test_create_bucket_conflict(self):
294327
client._connection.API_BASE_URL,
295328
'storage',
296329
client._connection.API_VERSION,
297-
'b?project=%s' % (PROJECT,),
330+
'b?project=%s' % (OTHER_PROJECT,),
298331
])
299332
data = {'error': {'message': 'Conflict'}}
300333
sent = {'name': BUCKET_NAME}
301334
http = _make_requests_session([
302335
_make_json_response(data, status=http_client.CONFLICT)])
303336
client._http_internal = http
304337

305-
self.assertRaises(Conflict, client.create_bucket, BUCKET_NAME)
338+
with self.assertRaises(Conflict):
339+
client.create_bucket(BUCKET_NAME, project=OTHER_PROJECT)
340+
306341
http.request.assert_called_once_with(
307342
method='POST', url=URI, data=mock.ANY, headers=mock.ANY)
308343
json_sent = http.request.call_args_list[0][1]['data']
@@ -337,6 +372,13 @@ def test_create_bucket_success(self):
337372
json_sent = http.request.call_args_list[0][1]['data']
338373
self.assertEqual(sent, json.loads(json_sent))
339374

375+
def test_list_buckets_wo_project(self):
376+
CREDENTIALS = _make_credentials()
377+
client = self._make_one(project=None, credentials=CREDENTIALS)
378+
379+
with self.assertRaises(ValueError):
380+
client.list_buckets()
381+
340382
def test_list_buckets_empty(self):
341383
from six.moves.urllib.parse import parse_qs
342384
from six.moves.urllib.parse import urlparse
@@ -371,6 +413,41 @@ def test_list_buckets_empty(self):
371413
uri_parts = urlparse(requested_url)
372414
self.assertEqual(parse_qs(uri_parts.query), expected_query)
373415

416+
def test_list_buckets_explicit_project(self):
417+
from six.moves.urllib.parse import parse_qs
418+
from six.moves.urllib.parse import urlparse
419+
420+
PROJECT = 'PROJECT'
421+
OTHER_PROJECT = 'OTHER_PROJECT'
422+
CREDENTIALS = _make_credentials()
423+
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)
424+
425+
http = _make_requests_session([_make_json_response({})])
426+
client._http_internal = http
427+
428+
buckets = list(client.list_buckets(project=OTHER_PROJECT))
429+
430+
self.assertEqual(len(buckets), 0)
431+
432+
http.request.assert_called_once_with(
433+
method='GET', url=mock.ANY, data=mock.ANY, headers=mock.ANY)
434+
435+
requested_url = http.request.mock_calls[0][2]['url']
436+
expected_base_url = '/'.join([
437+
client._connection.API_BASE_URL,
438+
'storage',
439+
client._connection.API_VERSION,
440+
'b',
441+
])
442+
self.assertTrue(requested_url.startswith(expected_base_url))
443+
444+
expected_query = {
445+
'project': [OTHER_PROJECT],
446+
'projection': ['noAcl'],
447+
}
448+
uri_parts = urlparse(requested_url)
449+
self.assertEqual(parse_qs(uri_parts.query), expected_query)
450+
374451
def test_list_buckets_non_empty(self):
375452
PROJECT = 'PROJECT'
376453
CREDENTIALS = _make_credentials()

storage/tests/unit/test_notification.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ def _make_bucket(self, client, name=BUCKET_NAME, user_project=None):
7474
bucket.user_project = user_project
7575
return bucket
7676

77+
def test_ctor_w_missing_project(self):
78+
client = self._make_client(project=None)
79+
bucket = self._make_bucket(client)
80+
81+
with self.assertRaises(ValueError):
82+
self._make_one(bucket, self.TOPIC_NAME)
83+
7784
def test_ctor_defaults(self):
7885
from google.cloud.storage.notification import NONE_PAYLOAD_FORMAT
7986

0 commit comments

Comments
 (0)