From 779962241e0721d5eaf4c3054d0cb0004bf046ed Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 18 Mar 2015 14:35:04 -0400 Subject: [PATCH 1/8] Add 'pubsub.subscription.Subscription' class. Include 'create', 'delete', 'exists', and 'reload' method to manage subscription state. Allow toggling between push / pull w/ 'modify_push_config'. Consume / acknowledge messages in pull-mode w/ 'pull', 'acknowledge', and 'modify_ack_deadline'. --- gcloud/pubsub/subscription.py | 181 +++++++++++++++++ gcloud/pubsub/test_subscription.py | 299 +++++++++++++++++++++++++++++ 2 files changed, 480 insertions(+) create mode 100644 gcloud/pubsub/subscription.py create mode 100644 gcloud/pubsub/test_subscription.py diff --git a/gcloud/pubsub/subscription.py b/gcloud/pubsub/subscription.py new file mode 100644 index 000000000000..1279881247f3 --- /dev/null +++ b/gcloud/pubsub/subscription.py @@ -0,0 +1,181 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Define API Subscriptions.""" + +from gcloud.exceptions import NotFound + + +class Subscription(object): + """Subscriptions receive messages published to their topics. + + See: + https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions + + :type name: string + :param name: the name of the subscription + + :type topic: :class:`gcloud.pubsub.topic.Topic` + :param topic: the topic to which the subscription belongs.. + + :type ack_deadline: int + :param ack_deadline: the deadline (in seconds) by which messages pulled + from the back-end must be ACKed. + + :type push_endpoint: string + :param push_endpoint: URL to which messages will be pushed by the back-end. + If not set, the application must pull messages. + """ + def __init__(self, name, topic, ack_deadline=None, push_endpoint=None): + self.name = name + self.topic = topic + self.ack_deadline = ack_deadline + self.push_endpoint = push_endpoint + + @property + def path(self): + """URL path for the subscription's APIs""" + project = self.topic.project + return '/projects/%s/subscriptions/%s' % (project, self.name) + + def create(self): + """API call: create the subscription via a PUT request + + See: + https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/create + """ + data = {'topic': self.topic.path} + + if self.ack_deadline is not None: + data['ackDeadline'] = self.ack_deadline + + if self.push_endpoint is not None: + data['pushConfig'] = {'pushEndpoint': self.push_endpoint} + + conn = self.topic.connection + conn.api_request(method='PUT', path=self.path, data=data) + + def exists(self): + """API call: test existence of the subsription via a GET request + + See + https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/get + """ + conn = self.topic.connection + try: + conn.api_request(method='GET', + path=self.path, + query_params={'fields': 'name'}) + except NotFound: + return False + else: + return True + + def reload(self): + """API call: test existence of the subsription via a GET request + + See + https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/get + """ + conn = self.topic.connection + data = conn.api_request(method='GET', path=self.path) + self.ack_deadline = data.get('ackDeadline') + push_config = data.get('pushConfig', {}) + self.push_endpoint = push_config.get('pushEndpoint') + + def modify_push_configuration(self, push_endpoint): + """API call: update the push endpoint for the subscription. + + See: + https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/modifyPushConfig + """ + data = {} + config = data['pushConfig'] = {} + if push_endpoint is not None: + config['pushEndpoint'] = push_endpoint + conn = self.topic.connection + conn.api_request(method='POST', + path='%s:modifyPushConfig' % self.path, + data=data) + self.push_endpoint = push_endpoint + + def pull(self, return_immediately=False, max_messages=1): + """API call: retrieve messages for the subscription. + + See: + https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/pull + + :type return_immediately: boolean + :param return_immediately: if True, the back-end returns even if no + messages are available; if False, the API + call blocks until one or more messages are + available. + + :type max_messages: int + :param max_messages: the maximum number of messages to return. + + :rtype: list of dict + :returns: sequence of mappings, each containing keys ``ackId`` (the + ID to be used in a subsequent call to :meth:`acknowledge`) + and ``message``. + """ + data = {'returnImmediately': return_immediately, + 'maxMessages': max_messages} + conn = self.topic.connection + response = conn.api_request(method='POST', + path='%s:pull' % self.path, + data=data) + return response['receivedMessages'] + + def acknowledge(self, ack_ids): + """API call: acknowledge retrieved messages for the subscription. + + See: + https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/acknowledge + + :type ack_ids: list of string + :param ack_ids: ack IDs of messages being acknowledged + """ + data = {'ackIds': ack_ids} + conn = self.topic.connection + conn.api_request(method='POST', + path='%s:acknowledge' % self.path, + data=data) + + def modify_ack_deadline(self, ack_id, ack_deadline): + """API call: acknowledge retrieved messages for the subscription. + + See: + https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/acknowledge + + :type ack_id: string + :param ack_id: ack ID of message being updated + + :type ack_deadline: int + :param ack_deadline: new deadline for the message, in seconds + """ + data = {'ackId': ack_id, 'ackDeadlineSeconds': ack_deadline} + conn = self.topic.connection + conn.api_request(method='POST', + path='%s:modifyAckDeadline' % self.path, + data=data) + + def delete(self): + """API call: delete the subscription via a DELETE request + + See: + https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/delete + """ + conn = self.topic.connection + conn.api_request(method='DELETE', path=self.path) diff --git a/gcloud/pubsub/test_subscription.py b/gcloud/pubsub/test_subscription.py new file mode 100644 index 000000000000..cc9147231b03 --- /dev/null +++ b/gcloud/pubsub/test_subscription.py @@ -0,0 +1,299 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest2 + + +class TestSubscription(unittest2.TestCase): + + def _getTargetClass(self): + from gcloud.pubsub.subscription import Subscription + return Subscription + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def test_ctor_defaults(self): + SUB_NAME = 'sub_name' + topic = object() + subscription = self._makeOne(SUB_NAME, topic) + self.assertEqual(subscription.name, SUB_NAME) + self.assertTrue(subscription.topic is topic) + self.assertEqual(subscription.ack_deadline, None) + self.assertEqual(subscription.push_endpoint, None) + + def test_ctor_explicit(self): + SUB_NAME = 'sub_name' + DEADLINE = 42 + ENDPOINT = 'https://api.example.com/push' + topic = object() + subscription = self._makeOne(SUB_NAME, topic, DEADLINE, ENDPOINT) + self.assertEqual(subscription.name, SUB_NAME) + self.assertTrue(subscription.topic is topic) + self.assertEqual(subscription.ack_deadline, DEADLINE) + self.assertEqual(subscription.push_endpoint, ENDPOINT) + + def test_create_pull_wo_ack_deadline(self): + PROJECT = 'PROJECT' + SUB_NAME = 'sub_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + TOPIC_PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME) + BODY = {'topic': TOPIC_PATH} + conn = _Connection({'name': SUB_PATH}) + topic = _Topic(TOPIC_NAME, project=PROJECT, connection=conn) + subscription = self._makeOne(SUB_NAME, topic) + subscription.create() + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'PUT') + self.assertEqual(req['path'], '/%s' % SUB_PATH) + self.assertEqual(req['data'], BODY) + + def test_create_push_w_ack_deadline(self): + PROJECT = 'PROJECT' + SUB_NAME = 'sub_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + TOPIC_PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME) + DEADLINE = 42 + ENDPOINT = 'https://api.example.com/push' + BODY = {'topic': TOPIC_PATH, + 'ackDeadline': DEADLINE, + 'pushConfig': {'pushEndpoint': ENDPOINT}} + conn = _Connection({'name': SUB_PATH}) + topic = _Topic(TOPIC_NAME, project=PROJECT, connection=conn) + subscription = self._makeOne(SUB_NAME, topic, DEADLINE, ENDPOINT) + subscription.create() + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'PUT') + self.assertEqual(req['path'], '/%s' % SUB_PATH) + self.assertEqual(req['data'], BODY) + + def test_exists_miss(self): + PROJECT = 'PROJECT' + SUB_NAME = 'sub_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + conn = _Connection() + topic = _Topic(TOPIC_NAME, project=PROJECT, connection=conn) + subscription = self._makeOne(SUB_NAME, topic) + self.assertFalse(subscription.exists()) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % SUB_PATH) + self.assertEqual(req['query_params'], {'fields': 'name'}) + + def test_exists_hit(self): + PROJECT = 'PROJECT' + SUB_NAME = 'sub_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + TOPIC_PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME) + conn = _Connection({'name': SUB_PATH, 'topic': TOPIC_PATH}) + topic = _Topic(TOPIC_NAME, project=PROJECT, connection=conn) + subscription = self._makeOne(SUB_NAME, topic) + self.assertTrue(subscription.exists()) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % SUB_PATH) + self.assertEqual(req['query_params'], {'fields': 'name'}) + + def test_reload(self): + PROJECT = 'PROJECT' + SUB_NAME = 'sub_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + TOPIC_PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME) + DEADLINE = 42 + ENDPOINT = 'https://api.example.com/push' + conn = _Connection({'name': SUB_PATH, + 'topic': TOPIC_PATH, + 'ackDeadline': DEADLINE, + 'pushConfig': {'pushEndpoint': ENDPOINT}}) + topic = _Topic(TOPIC_NAME, project=PROJECT, connection=conn) + subscription = self._makeOne(SUB_NAME, topic) + subscription.reload() + self.assertEqual(subscription.ack_deadline, DEADLINE) + self.assertEqual(subscription.push_endpoint, ENDPOINT) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % SUB_PATH) + + def test_modify_push_config_w_endpoint(self): + PROJECT = 'PROJECT' + SUB_NAME = 'sub_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + ENDPOINT = 'https://api.example.com/push' + conn = _Connection({}) + topic = _Topic(TOPIC_NAME, project=PROJECT, connection=conn) + subscription = self._makeOne(SUB_NAME, topic) + subscription.modify_push_configuration(push_endpoint=ENDPOINT) + self.assertEqual(subscription.push_endpoint, ENDPOINT) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s:modifyPushConfig' % SUB_PATH) + self.assertEqual(req['data'], + {'pushConfig': {'pushEndpoint': ENDPOINT}}) + + def test_modify_push_config_wo_endpoint(self): + PROJECT = 'PROJECT' + SUB_NAME = 'sub_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + ENDPOINT = 'https://api.example.com/push' + conn = _Connection({}) + topic = _Topic(TOPIC_NAME, project=PROJECT, connection=conn) + subscription = self._makeOne(SUB_NAME, topic, push_endpoint=ENDPOINT) + subscription.modify_push_configuration(push_endpoint=None) + self.assertEqual(subscription.push_endpoint, None) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s:modifyPushConfig' % SUB_PATH) + self.assertEqual(req['data'], {'pushConfig': {}}) + + def test_pull_wo_return_immediately_wo_max_messages(self): + import base64 + PROJECT = 'PROJECT' + SUB_NAME = 'sub_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + ACK_ID = 'DEADBEEF' + MSG_ID = 'BEADCAFE' + PAYLOAD = b'This is the message text' + B64 = base64.b64encode(PAYLOAD) + MESSAGE = {'messageId': MSG_ID, 'data': B64, 'attributes': {}} + REC_MESSAGE = {'ackId': ACK_ID, 'message': MESSAGE} + conn = _Connection({'receivedMessages': [REC_MESSAGE]}) + topic = _Topic(TOPIC_NAME, project=PROJECT, connection=conn) + subscription = self._makeOne(SUB_NAME, topic) + pulled = subscription.pull() + self.assertEqual(len(pulled), 1) + self.assertEqual(pulled[0]['ackId'], ACK_ID) + self.assertEqual(pulled[0]['message'], MESSAGE) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s:pull' % SUB_PATH) + self.assertEqual(req['data'], + {'returnImmediately': False, 'maxMessages': 1}) + + def test_pull_w_return_immediately_w_max_messages(self): + import base64 + PROJECT = 'PROJECT' + SUB_NAME = 'sub_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + ACK_ID = 'DEADBEEF' + MSG_ID = 'BEADCAFE' + PAYLOAD = b'This is the message text' + B64 = base64.b64encode(PAYLOAD) + MESSAGE = {'messageId': MSG_ID, 'data': B64, 'attributes': {}} + REC_MESSAGE = {'ackId': ACK_ID, 'message': MESSAGE} + conn = _Connection({'receivedMessages': [REC_MESSAGE]}) + topic = _Topic(TOPIC_NAME, project=PROJECT, connection=conn) + subscription = self._makeOne(SUB_NAME, topic) + pulled = subscription.pull(return_immediately=True, max_messages=3) + self.assertEqual(len(pulled), 1) + self.assertEqual(pulled[0]['ackId'], ACK_ID) + self.assertEqual(pulled[0]['message'], MESSAGE) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s:pull' % SUB_PATH) + self.assertEqual(req['data'], + {'returnImmediately': True, 'maxMessages': 3}) + + def test_acknowledge(self): + PROJECT = 'PROJECT' + SUB_NAME = 'sub_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + ACK_ID1 = 'DEADBEEF' + ACK_ID2 = 'BEADCAFE' + conn = _Connection({}) + topic = _Topic(TOPIC_NAME, project=PROJECT, connection=conn) + subscription = self._makeOne(SUB_NAME, topic) + subscription.acknowledge([ACK_ID1, ACK_ID2]) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s:acknowledge' % SUB_PATH) + self.assertEqual(req['data'], {'ackIds': [ACK_ID1, ACK_ID2]}) + + def test_modify_ack_deadline(self): + PROJECT = 'PROJECT' + SUB_NAME = 'sub_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + ACK_ID = 'DEADBEEF' + DEADLINE = 42 + conn = _Connection({}) + topic = _Topic(TOPIC_NAME, project=PROJECT, connection=conn) + subscription = self._makeOne(SUB_NAME, topic) + subscription.modify_ack_deadline(ACK_ID, DEADLINE) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s:modifyAckDeadline' % SUB_PATH) + self.assertEqual(req['data'], + {'ackId': ACK_ID, 'ackDeadlineSeconds': DEADLINE}) + + def test_delete(self): + PROJECT = 'PROJECT' + SUB_NAME = 'sub_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + conn = _Connection({}) + topic = _Topic(TOPIC_NAME, project=PROJECT, connection=conn) + subscription = self._makeOne(SUB_NAME, topic) + subscription.delete() + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'DELETE') + self.assertEqual(req['path'], '/%s' % SUB_PATH) + + +class _Connection(object): + + def __init__(self, *responses): + self._responses = responses + self._requested = [] + + def api_request(self, **kw): + from gcloud.exceptions import NotFound + self._requested.append(kw) + + try: + response, self._responses = self._responses[0], self._responses[1:] + except: + raise NotFound('miss') + else: + return response + + +class _Topic(object): + + def __init__(self, name, project, connection): + self.name = name + self.project = project + self.connection = connection + self.path = 'projects/%s/topics/%s' % (project, name) From 42b0dc3cfd3d838b2f24c8ae85518327ea8fbef2 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 18 Mar 2015 14:35:35 -0400 Subject: [PATCH 2/8] Add 'pubsub.api.list_subscriptions'. --- gcloud/pubsub/api.py | 46 ++++++++++++++++++++++ gcloud/pubsub/test_api.py | 80 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/gcloud/pubsub/api.py b/gcloud/pubsub/api.py index f331652ddbcd..d641e1be2679 100644 --- a/gcloud/pubsub/api.py +++ b/gcloud/pubsub/api.py @@ -52,3 +52,49 @@ def list_topics(page_size=None, page_token=None, path = '/projects/%s/topics' % project return connection.api_request(method='GET', path=path, query_params=params) + + +def list_subscriptions(page_size=None, page_token=None, topic_name=None, + project=None, connection=None): + """List subscriptions for a given project. + + :type page_size: int + :param page_size: maximum number of topics to return, If not passed, + defaults to a value set by the API. + + :type page_token: string + :param page_token: opaque marker for the next "page" of topics. If not + passed, the API will return the first page of topics. + + :type topic_name: string + :param topic_name: limit results to subscriptions bound to the given topic. + + :type project: string + :param project: project ID to query. If not passed, defaults to the + project ID inferred from the environment. + + :type connection: :class:`gcloud.pubsub.connection.Connection` + :param connection: connection to use for the query. If not passed, + defaults to the connection inferred from the + environment. + + :rtype: dict + :returns: keys include ``subscriptions`` (a list of subscription mappings) + and ``nextPageToken`` (a string: if non-empty, indicates that + more topics can be retrieved with another call (pass that + value as ``page_token``). + """ + params = {} + + if page_size is not None: + params['pageSize'] = page_size + + if page_token is not None: + params['pageToken'] = page_token + + if topic_name is None: + path = '/projects/%s/subscriptions' % project + else: + path = '/projects/%s/topics/%s/subscriptions' % (project, topic_name) + + return connection.api_request(method='GET', path=path, query_params=params) diff --git a/gcloud/pubsub/test_api.py b/gcloud/pubsub/test_api.py index d21956073db1..a244a5cd7392 100644 --- a/gcloud/pubsub/test_api.py +++ b/gcloud/pubsub/test_api.py @@ -61,6 +61,86 @@ def test_w_explicit_connection_w_paging(self): {'pageSize': SIZE, 'pageToken': TOKEN1}) +class Test_list_subscriptions(unittest2.TestCase): + + def _callFUT(self, *args, **kw): + from gcloud.pubsub.api import list_subscriptions + return list_subscriptions(*args, **kw) + + def test_w_explicit_connection_no_paging(self): + PROJECT = 'PROJECT' + SUB_NAME = 'topic_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + TOPIC_PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME) + TOKEN = 'TOKEN' + returned = {'subscriptions': [{'name': SUB_PATH, 'topic': TOPIC_PATH}], + 'nextPageToken': TOKEN} + conn = _Connection(returned) + response = self._callFUT(project=PROJECT, connection=conn) + subscriptions = response['subscriptions'] + self.assertEqual(len(subscriptions), 1) + self.assertEqual(subscriptions[0], + {'name': SUB_PATH, 'topic': TOPIC_PATH}) + self.assertEqual(response['nextPageToken'], TOKEN) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/projects/%s/subscriptions' % PROJECT) + self.assertEqual(req['query_params'], {}) + + def test_w_explicit_connection_w_paging(self): + PROJECT = 'PROJECT' + SUB_NAME = 'topic_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + TOPIC_PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME) + TOKEN1 = 'TOKEN1' + TOKEN2 = 'TOKEN2' + SIZE = 1 + returned = {'subscriptions': [{'name': SUB_PATH, 'topic': TOPIC_PATH}], + 'nextPageToken': TOKEN2} + conn = _Connection(returned) + response = self._callFUT(SIZE, TOKEN1, + project=PROJECT, connection=conn) + subscriptions = response['subscriptions'] + self.assertEqual(len(subscriptions), 1) + self.assertEqual(subscriptions[0], + {'name': SUB_PATH, 'topic': TOPIC_PATH}) + self.assertEqual(response['nextPageToken'], TOKEN2) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/projects/%s/subscriptions' % PROJECT) + self.assertEqual(req['query_params'], + {'pageSize': SIZE, 'pageToken': TOKEN1}) + + def test_w_topic_name(self): + PROJECT = 'PROJECT' + SUB_NAME = 'topic_name' + SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME) + TOPIC_NAME = 'topic_name' + TOPIC_PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME) + TOKEN = 'TOKEN' + returned = {'subscriptions': [{'name': SUB_PATH, 'topic': TOPIC_PATH}], + 'nextPageToken': TOKEN} + conn = _Connection(returned) + response = self._callFUT(topic_name=TOPIC_NAME, + project=PROJECT, connection=conn) + subscriptions = response['subscriptions'] + self.assertEqual(len(subscriptions), 1) + self.assertEqual(subscriptions[0], + {'name': SUB_PATH, 'topic': TOPIC_PATH}) + self.assertEqual(response['nextPageToken'], TOKEN) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], + '/projects/%s/topics/%s/subscriptions' + % (PROJECT, TOPIC_NAME)) + self.assertEqual(req['query_params'], {}) + + class _Connection(object): def __init__(self, *responses): From db753507c7f1ca22b03682033fe97821f9be00a1 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 19 Mar 2015 14:09:52 -0400 Subject: [PATCH 3/8] Link to API docs for 'list_topics'/'list_subscriptions'. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/743#discussion_r26779135 --- gcloud/pubsub/api.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gcloud/pubsub/api.py b/gcloud/pubsub/api.py index d641e1be2679..32b11f1d286e 100644 --- a/gcloud/pubsub/api.py +++ b/gcloud/pubsub/api.py @@ -19,6 +19,9 @@ def list_topics(page_size=None, page_token=None, project=None, connection=None): """List topics for a given project. + See: + https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/topics/list + :type page_size: int :param page_size: maximum number of topics to return, If not passed, defaults to a value set by the API. @@ -58,6 +61,12 @@ def list_subscriptions(page_size=None, page_token=None, topic_name=None, project=None, connection=None): """List subscriptions for a given project. + See: + https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/topics/list + + and (where ``topic_name`` is passed): + https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/topics/subscriptions/list + :type page_size: int :param page_size: maximum number of topics to return, If not passed, defaults to a value set by the API. From bdcc60bb848a0d289a4939b7e188e14d3fa12643 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 19 Mar 2015 14:11:38 -0400 Subject: [PATCH 4/8] Spell out ACK. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/743#discussion_r26779269. --- gcloud/pubsub/subscription.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcloud/pubsub/subscription.py b/gcloud/pubsub/subscription.py index 1279881247f3..d610f5418a4f 100644 --- a/gcloud/pubsub/subscription.py +++ b/gcloud/pubsub/subscription.py @@ -31,7 +31,7 @@ class Subscription(object): :type ack_deadline: int :param ack_deadline: the deadline (in seconds) by which messages pulled - from the back-end must be ACKed. + from the back-end must be acknowledged. :type push_endpoint: string :param push_endpoint: URL to which messages will be pushed by the back-end. From e1b581dddefb3130d023d29da9809a69c46edea9 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 19 Mar 2015 14:13:58 -0400 Subject: [PATCH 5/8] Document param to 'modify_push_configuration'. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/743#discussion_r26779358 --- gcloud/pubsub/subscription.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gcloud/pubsub/subscription.py b/gcloud/pubsub/subscription.py index d610f5418a4f..71b62c66fcb0 100644 --- a/gcloud/pubsub/subscription.py +++ b/gcloud/pubsub/subscription.py @@ -99,6 +99,11 @@ def modify_push_configuration(self, push_endpoint): See: https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/modifyPushConfig + + :type push_endpoint: string + :param push_endpoint: URL to which messages will be pushed by the + back-end. If None, the application must pull + messages. """ data = {} config = data['pushConfig'] = {} From 6a9eb1904c291936a8abd7082ad1e221f0627f7f Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 19 Mar 2015 14:19:02 -0400 Subject: [PATCH 6/8] Fix docstring for 'Subscription.refresh'. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/743#discussion_r26781205 --- gcloud/pubsub/subscription.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gcloud/pubsub/subscription.py b/gcloud/pubsub/subscription.py index 71b62c66fcb0..4a856f82b400 100644 --- a/gcloud/pubsub/subscription.py +++ b/gcloud/pubsub/subscription.py @@ -67,7 +67,7 @@ def create(self): conn.api_request(method='PUT', path=self.path, data=data) def exists(self): - """API call: test existence of the subsription via a GET request + """API call: test existence of the subscription via a GET request See https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/get @@ -83,7 +83,7 @@ def exists(self): return True def reload(self): - """API call: test existence of the subsription via a GET request + """API call: sync local subscription configuration via a GET request See https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/get From ea1625cc6c708ae4931d64ad8e506e5df06aab00 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 19 Mar 2015 14:21:16 -0400 Subject: [PATCH 7/8] Fix docstring for 'Subscription.modify_ack_deadline'. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/743#discussion_r26781540 --- gcloud/pubsub/subscription.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcloud/pubsub/subscription.py b/gcloud/pubsub/subscription.py index 4a856f82b400..6655b0d0983e 100644 --- a/gcloud/pubsub/subscription.py +++ b/gcloud/pubsub/subscription.py @@ -159,7 +159,7 @@ def acknowledge(self, ack_ids): data=data) def modify_ack_deadline(self, ack_id, ack_deadline): - """API call: acknowledge retrieved messages for the subscription. + """API call: update acknowledgement deadline for a retrieved messages. See: https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/acknowledge From 7821d8878a92f70288095c31ac47905a3f5fa915 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 19 Mar 2015 15:57:48 -0400 Subject: [PATCH 8/8] Docstring typo fixes per @tmatsuo. [ci skip] --- gcloud/pubsub/subscription.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gcloud/pubsub/subscription.py b/gcloud/pubsub/subscription.py index 6655b0d0983e..7464a4e46879 100644 --- a/gcloud/pubsub/subscription.py +++ b/gcloud/pubsub/subscription.py @@ -159,7 +159,7 @@ def acknowledge(self, ack_ids): data=data) def modify_ack_deadline(self, ack_id, ack_deadline): - """API call: update acknowledgement deadline for a retrieved messages. + """API call: update acknowledgement deadline for a retrieved message. See: https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/acknowledge @@ -177,7 +177,7 @@ def modify_ack_deadline(self, ack_id, ack_deadline): data=data) def delete(self): - """API call: delete the subscription via a DELETE request + """API call: delete the subscription via a DELETE request. See: https://cloud.google.com/pubsub/reference/rest/v1beta2/projects/subscriptions/delete