Skip to content

Commit f9e9103

Browse files
committed
Add 'Topic.list_subscriptions'.
Make it handle the fact that the API returns only subscription paths, not resources. Drop the 'topic_name' argument to 'Client.list_subscriptions'. Fixes #1485.
1 parent c18e96e commit f9e9103

8 files changed

Lines changed: 225 additions & 61 deletions

File tree

docs/pubsub-usage.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ List subscriptions for a topic:
186186

187187
>>> from gcloud import pubsub
188188
>>> client = pubsub.Client()
189-
>>> subscriptions, next_page_token = client.list_subscriptions(
190-
... topic_name='topic_name') # API request
189+
>>> topic = client.topic('topic_name')
190+
>>> subscriptions, next_page_token = topic.list_subscriptions() # API request
191191
>>> [subscription.name for subscription in subscriptions]
192192
['subscription_name']
193193

gcloud/pubsub/_helpers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,34 @@ def topic_name_from_path(path, project):
4343
'project from resource.')
4444

4545
return path_parts[3]
46+
47+
48+
def subscription_name_from_path(path, project):
49+
"""Validate a subscription URI path and get the subscription name.
50+
51+
:type path: string
52+
:param path: URI path for a subscription API request.
53+
54+
:type project: string
55+
:param project: The project associated with the request. It is
56+
included for validation purposes.
57+
58+
:rtype: string
59+
:returns: subscription name parsed from ``path``.
60+
:raises: :class:`ValueError` if the ``path`` is ill-formed or if
61+
the project from the ``path`` does not agree with the
62+
``project`` passed in.
63+
"""
64+
# PATH = 'projects/%s/subscriptions/%s' % (PROJECT, TOPIC_NAME)
65+
path_parts = path.split('/')
66+
if (len(path_parts) != 4 or path_parts[0] != 'projects' or
67+
path_parts[2] != 'subscriptions'):
68+
raise ValueError(
69+
'Expected path to be of the form '
70+
'projects/{project}/subscriptions/{subscription_name}')
71+
if (len(path_parts) != 4 or path_parts[0] != 'projects' or
72+
path_parts[2] != 'subscriptions' or path_parts[1] != project):
73+
raise ValueError('Project from client should agree with '
74+
'project from resource.')
75+
76+
return path_parts[3]

gcloud/pubsub/client.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ def list_topics(self, page_size=None, page_token=None):
8080
for resource in resp.get('topics', ())]
8181
return topics, resp.get('nextPageToken')
8282

83-
def list_subscriptions(self, page_size=None, page_token=None,
84-
topic_name=None):
83+
def list_subscriptions(self, page_size=None, page_token=None):
8584
"""List subscriptions for the project associated with this client.
8685
8786
See:
@@ -99,10 +98,6 @@ def list_subscriptions(self, page_size=None, page_token=None,
9998
passed, the API will return the first page of
10099
topics.
101100
102-
:type topic_name: string
103-
:param topic_name: limit results to subscriptions bound to the given
104-
topic.
105-
106101
:rtype: tuple, (list, str)
107102
:returns: list of :class:`gcloud.pubsub.subscription.Subscription`,
108103
plus a "next page token" string: if not None, indicates that
@@ -117,11 +112,7 @@ def list_subscriptions(self, page_size=None, page_token=None,
117112
if page_token is not None:
118113
params['pageToken'] = page_token
119114

120-
if topic_name is None:
121-
path = '/projects/%s/subscriptions' % (self.project,)
122-
else:
123-
path = '/projects/%s/topics/%s/subscriptions' % (self.project,
124-
topic_name)
115+
path = '/projects/%s/subscriptions' % (self.project,)
125116

126117
resp = self.connection.api_request(method='GET', path=path,
127118
query_params=params)

gcloud/pubsub/test__helpers.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,35 @@ def test_valid_data(self):
4545
PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME)
4646
topic_name = self._callFUT(PATH, PROJECT)
4747
self.assertEqual(topic_name, TOPIC_NAME)
48+
49+
50+
class Test_subscription_name_from_path(unittest2.TestCase):
51+
52+
def _callFUT(self, path, project):
53+
from gcloud.pubsub._helpers import subscription_name_from_path
54+
return subscription_name_from_path(path, project)
55+
56+
def test_invalid_path_length(self):
57+
PATH = 'projects/foo'
58+
PROJECT = None
59+
self.assertRaises(ValueError, self._callFUT, PATH, PROJECT)
60+
61+
def test_invalid_path_format(self):
62+
TOPIC_NAME = 'TOPIC_NAME'
63+
PROJECT = 'PROJECT'
64+
PATH = 'foo/%s/bar/%s' % (PROJECT, TOPIC_NAME)
65+
self.assertRaises(ValueError, self._callFUT, PATH, PROJECT)
66+
67+
def test_invalid_project(self):
68+
TOPIC_NAME = 'TOPIC_NAME'
69+
PROJECT1 = 'PROJECT1'
70+
PROJECT2 = 'PROJECT2'
71+
PATH = 'projects/%s/subscriptions/%s' % (PROJECT1, TOPIC_NAME)
72+
self.assertRaises(ValueError, self._callFUT, PATH, PROJECT2)
73+
74+
def test_valid_data(self):
75+
TOPIC_NAME = 'TOPIC_NAME'
76+
PROJECT = 'PROJECT'
77+
PATH = 'projects/%s/subscriptions/%s' % (PROJECT, TOPIC_NAME)
78+
subscription_name = self._callFUT(PATH, PROJECT)
79+
self.assertEqual(subscription_name, TOPIC_NAME)

gcloud/pubsub/test_client.py

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -196,47 +196,6 @@ def test_list_subscriptions_w_missing_key(self):
196196
self.assertEqual(req['path'], '/projects/%s/subscriptions' % PROJECT)
197197
self.assertEqual(req['query_params'], {})
198198

199-
def test_list_subscriptions_with_topic_name(self):
200-
from gcloud.pubsub.subscription import Subscription
201-
PROJECT = 'PROJECT'
202-
CREDS = _Credentials()
203-
204-
CLIENT_OBJ = self._makeOne(project=PROJECT, credentials=CREDS)
205-
206-
SUB_NAME_1 = 'subscription_1'
207-
SUB_PATH_1 = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME_1)
208-
SUB_NAME_2 = 'subscription_2'
209-
SUB_PATH_2 = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME_2)
210-
TOPIC_NAME = 'topic_name'
211-
TOPIC_PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME)
212-
SUB_INFO = [{'name': SUB_PATH_1, 'topic': TOPIC_PATH},
213-
{'name': SUB_PATH_2, 'topic': TOPIC_PATH}]
214-
TOKEN = 'TOKEN'
215-
RETURNED = {'subscriptions': SUB_INFO, 'nextPageToken': TOKEN}
216-
# Replace the connection on the client with one of our own.
217-
CLIENT_OBJ.connection = _Connection(RETURNED)
218-
219-
# Execute request.
220-
subscriptions, next_page_token = CLIENT_OBJ.list_subscriptions(
221-
topic_name=TOPIC_NAME)
222-
# Test values are correct.
223-
self.assertEqual(len(subscriptions), 2)
224-
self.assertTrue(isinstance(subscriptions[0], Subscription))
225-
self.assertEqual(subscriptions[0].name, SUB_NAME_1)
226-
self.assertEqual(subscriptions[0].topic.name, TOPIC_NAME)
227-
self.assertTrue(isinstance(subscriptions[1], Subscription))
228-
self.assertEqual(subscriptions[1].name, SUB_NAME_2)
229-
self.assertEqual(subscriptions[1].topic.name, TOPIC_NAME)
230-
self.assertTrue(subscriptions[1].topic is subscriptions[0].topic)
231-
self.assertEqual(next_page_token, TOKEN)
232-
self.assertEqual(len(CLIENT_OBJ.connection._requested), 1)
233-
req = CLIENT_OBJ.connection._requested[0]
234-
self.assertEqual(req['method'], 'GET')
235-
self.assertEqual(req['path'],
236-
'/projects/%s/topics/%s/subscriptions'
237-
% (PROJECT, TOPIC_NAME))
238-
self.assertEqual(req['query_params'], {})
239-
240199
def test_topic(self):
241200
PROJECT = 'PROJECT'
242201
TOPIC_NAME = 'TOPIC_NAME'

gcloud/pubsub/test_topic.py

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,15 +336,122 @@ def test_subscription(self):
336336
TOPIC_NAME = 'topic_name'
337337
PROJECT = 'PROJECT'
338338
CLIENT = _Client(project=PROJECT)
339-
topic = self._makeOne(TOPIC_NAME,
340-
client=CLIENT)
339+
topic = self._makeOne(TOPIC_NAME, client=CLIENT)
341340

342341
SUBSCRIPTION_NAME = 'subscription_name'
343342
subscription = topic.subscription(SUBSCRIPTION_NAME)
344343
self.assertTrue(isinstance(subscription, Subscription))
345344
self.assertEqual(subscription.name, SUBSCRIPTION_NAME)
346345
self.assertTrue(subscription.topic is topic)
347346

347+
def test_list_subscriptions_no_paging(self):
348+
from gcloud.pubsub.subscription import Subscription
349+
TOPIC_NAME = 'topic_name'
350+
PROJECT = 'PROJECT'
351+
SUB_NAME_1 = 'subscription_1'
352+
SUB_PATH_1 = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME_1)
353+
SUB_NAME_2 = 'subscription_2'
354+
SUB_PATH_2 = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME_2)
355+
TOPIC_NAME = 'topic_name'
356+
SUBS_LIST = [SUB_PATH_1, SUB_PATH_2]
357+
TOKEN = 'TOKEN'
358+
RETURNED = {'subscriptions': SUBS_LIST, 'nextPageToken': TOKEN}
359+
360+
conn = _Connection(RETURNED)
361+
CLIENT = _Client(project=PROJECT, connection=conn)
362+
topic = self._makeOne(TOPIC_NAME, client=CLIENT)
363+
364+
# Execute request.
365+
subscriptions, next_page_token = topic.list_subscriptions()
366+
# Test values are correct.
367+
self.assertEqual(len(subscriptions), 2)
368+
369+
subscription = subscriptions[0]
370+
self.assertTrue(isinstance(subscription, Subscription))
371+
self.assertEqual(subscriptions[0].name, SUB_NAME_1)
372+
self.assertTrue(subscription.topic is topic)
373+
374+
subscription = subscriptions[1]
375+
self.assertTrue(isinstance(subscription, Subscription))
376+
self.assertEqual(subscriptions[1].name, SUB_NAME_2)
377+
self.assertTrue(subscription.topic is topic)
378+
379+
self.assertEqual(next_page_token, TOKEN)
380+
self.assertEqual(len(conn._requested), 1)
381+
req = conn._requested[0]
382+
self.assertEqual(req['method'], 'GET')
383+
self.assertEqual(req['path'],
384+
'/projects/%s/topics/%s/subscriptions'
385+
% (PROJECT, TOPIC_NAME))
386+
self.assertEqual(req['query_params'], {})
387+
388+
def test_list_subscriptions_with_paging(self):
389+
from gcloud.pubsub.subscription import Subscription
390+
TOPIC_NAME = 'topic_name'
391+
PROJECT = 'PROJECT'
392+
SUB_NAME_1 = 'subscription_1'
393+
SUB_PATH_1 = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME_1)
394+
SUB_NAME_2 = 'subscription_2'
395+
SUB_PATH_2 = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME_2)
396+
TOPIC_NAME = 'topic_name'
397+
SUBS_LIST = [SUB_PATH_1, SUB_PATH_2]
398+
PAGE_SIZE = 10
399+
TOKEN = 'TOKEN'
400+
RETURNED = {'subscriptions': SUBS_LIST}
401+
402+
conn = _Connection(RETURNED)
403+
CLIENT = _Client(project=PROJECT, connection=conn)
404+
topic = self._makeOne(TOPIC_NAME, client=CLIENT)
405+
406+
# Execute request.
407+
subscriptions, next_page_token = topic.list_subscriptions(
408+
page_size=PAGE_SIZE, page_token=TOKEN)
409+
# Test values are correct.
410+
self.assertEqual(len(subscriptions), 2)
411+
412+
subscription = subscriptions[0]
413+
self.assertTrue(isinstance(subscription, Subscription))
414+
self.assertEqual(subscriptions[0].name, SUB_NAME_1)
415+
self.assertTrue(subscription.topic is topic)
416+
417+
subscription = subscriptions[1]
418+
self.assertTrue(isinstance(subscription, Subscription))
419+
self.assertEqual(subscriptions[1].name, SUB_NAME_2)
420+
self.assertTrue(subscription.topic is topic)
421+
422+
self.assertEqual(next_page_token, None)
423+
self.assertEqual(len(conn._requested), 1)
424+
req = conn._requested[0]
425+
self.assertEqual(req['method'], 'GET')
426+
self.assertEqual(req['path'],
427+
'/projects/%s/topics/%s/subscriptions'
428+
% (PROJECT, TOPIC_NAME))
429+
self.assertEqual(req['query_params'],
430+
{'pageSize': PAGE_SIZE, 'pageToken': TOKEN})
431+
432+
def test_list_subscriptions_missing_key(self):
433+
TOPIC_NAME = 'topic_name'
434+
PROJECT = 'PROJECT'
435+
TOPIC_NAME = 'topic_name'
436+
437+
conn = _Connection({})
438+
CLIENT = _Client(project=PROJECT, connection=conn)
439+
topic = self._makeOne(TOPIC_NAME, client=CLIENT)
440+
441+
# Execute request.
442+
subscriptions, next_page_token = topic.list_subscriptions()
443+
# Test values are correct.
444+
self.assertEqual(len(subscriptions), 0)
445+
self.assertEqual(next_page_token, None)
446+
447+
self.assertEqual(len(conn._requested), 1)
448+
req = conn._requested[0]
449+
self.assertEqual(req['method'], 'GET')
450+
self.assertEqual(req['path'],
451+
'/projects/%s/topics/%s/subscriptions'
452+
% (PROJECT, TOPIC_NAME))
453+
self.assertEqual(req['query_params'], {})
454+
348455

349456
class TestBatch(unittest2.TestCase):
350457

gcloud/pubsub/topic.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from gcloud._helpers import _datetime_to_rfc3339
2020
from gcloud._helpers import _NOW
2121
from gcloud.exceptions import NotFound
22+
from gcloud.pubsub._helpers import subscription_name_from_path
2223
from gcloud.pubsub._helpers import topic_name_from_path
2324
from gcloud.pubsub.subscription import Subscription
2425

@@ -212,6 +213,51 @@ def delete(self, client=None):
212213
client = self._require_client(client)
213214
client.connection.api_request(method='DELETE', path=self.path)
214215

216+
def list_subscriptions(self, page_size=None, page_token=None, client=None):
217+
"""List subscriptions for the project associated with this client.
218+
219+
See:
220+
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics.subscriptions/list
221+
222+
:type page_size: int
223+
:param page_size: maximum number of topics to return, If not passed,
224+
defaults to a value set by the API.
225+
226+
:type page_token: string
227+
:param page_token: opaque marker for the next "page" of topics. If not
228+
passed, the API will return the first page of
229+
topics.
230+
231+
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
232+
:param client: the client to use. If not passed, falls back to the
233+
``client`` stored on the current topic.
234+
235+
:rtype: tuple, (list, str)
236+
:returns: list of :class:`gcloud.pubsub.subscription.Subscription`,
237+
plus a "next page token" string: if not None, indicates that
238+
more topics can be retrieved with another call (pass that
239+
value as ``page_token``).
240+
"""
241+
client = self._require_client(client)
242+
params = {}
243+
244+
if page_size is not None:
245+
params['pageSize'] = page_size
246+
247+
if page_token is not None:
248+
params['pageToken'] = page_token
249+
250+
path = '/projects/%s/topics/%s/subscriptions' % (
251+
self.project, self.name)
252+
253+
resp = client.connection.api_request(method='GET', path=path,
254+
query_params=params)
255+
subscriptions = []
256+
for sub_path in resp.get('subscriptions', ()):
257+
sub_name = subscription_name_from_path(sub_path, self.project)
258+
subscriptions.append(Subscription(sub_name, self))
259+
return subscriptions, resp.get('nextPageToken')
260+
215261

216262
class Batch(object):
217263
"""Context manager: collect messages to publish via a single API call.

system_tests/pubsub.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,7 @@ def test_list_subscriptions(self):
118118
self.assertFalse(topic.exists())
119119
topic.create()
120120
self.to_delete.append(topic)
121-
empty, _ = Config.CLIENT.list_subscriptions(
122-
topic_name=DEFAULT_TOPIC_NAME)
121+
empty, _ = topic.list_subscriptions()
123122
self.assertEqual(len(empty), 0)
124123
subscriptions_to_create = [
125124
'new%d' % (1000 * time.time(),),
@@ -132,10 +131,9 @@ def test_list_subscriptions(self):
132131
self.to_delete.append(subscription)
133132

134133
# Retrieve the subscriptions.
135-
all_subscriptions, _ = Config.CLIENT.list_subscriptions()
134+
all_subscriptions, _ = topic.list_subscriptions()
136135
created = [subscription for subscription in all_subscriptions
137-
if subscription.name in subscriptions_to_create and
138-
subscription.topic.name == DEFAULT_TOPIC_NAME]
136+
if subscription.name in subscriptions_to_create]
139137
self.assertEqual(len(created), len(subscriptions_to_create))
140138

141139
def test_message_pull_mode_e2e(self):

0 commit comments

Comments
 (0)