Skip to content

Commit c03e7e0

Browse files
committed
Implementing topic.subscription factory.
Also using `topic.subscription` factory in docs and regression tests instead of instantiating a `Subscription`. Fixes #929.
1 parent dca74b0 commit c03e7e0

File tree

8 files changed

+154
-27
lines changed

8 files changed

+154
-27
lines changed

docs/pubsub-usage.rst

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ Create a new pull subscription for a topic:
121121
>>> from gcloud import pubsub
122122
>>> client = pubsub.Client()
123123
>>> topic = client.topic('topic_name')
124-
>>> subscription = pubsub.Subscription('subscription_name', topic)
124+
>>> subscription = topic.subscription('subscription_name')
125125
>>> subscription.create() # API request
126126

127127
Create a new pull subscription for a topic with a non-default ACK deadline:
@@ -131,8 +131,7 @@ Create a new pull subscription for a topic with a non-default ACK deadline:
131131
>>> from gcloud import pubsub
132132
>>> client = pubsub.Client()
133133
>>> topic = client.topic('topic_name')
134-
>>> subscription = pubsub.Subscription('subscription_name', topic,
135-
... ack_deadline=90)
134+
>>> subscription = topic.subscription('subscription_name', ack_deadline=90)
136135
>>> subscription.create() # API request
137136

138137
Create a new push subscription for a topic:
@@ -143,8 +142,8 @@ Create a new push subscription for a topic:
143142
>>> ENDPOINT = 'https://example.com/hook'
144143
>>> client = pubsub.Client()
145144
>>> topic = client.topic('topic_name')
146-
>>> subscription = pubsub.Subscription('subscription_name', topic,
147-
... push_endpoint=ENDPOINT)
145+
>>> subscription = topic.subscription('subscription_name',
146+
... push_endpoint=ENDPOINT)
148147
>>> subscription.create() # API request
149148

150149
Check for the existence of a subscription:
@@ -154,7 +153,7 @@ Check for the existence of a subscription:
154153
>>> from gcloud import pubsub
155154
>>> client = pubsub.Client()
156155
>>> topic = client.topic('topic_name')
157-
>>> subscription = pubsub.Subscription('subscription_name', topic)
156+
>>> subscription = topic.subscription('subscription_name')
158157
>>> subscription.exists() # API request
159158
True
160159

@@ -166,7 +165,7 @@ Convert a pull subscription to push:
166165
>>> ENDPOINT = 'https://example.com/hook'
167166
>>> client = pubsub.Client()
168167
>>> topic = client.topic('topic_name')
169-
>>> subscription = pubsub.Subscription('subscription_name', topic)
168+
>>> subscription = topic.subscription('subscription_name')
170169
>>> subscription.modify_push_configuration(push_endpoint=ENDPOINT) # API request
171170

172171
Convert a push subscription to pull:
@@ -177,8 +176,8 @@ Convert a push subscription to pull:
177176
>>> ENDPOINT = 'https://example.com/hook'
178177
>>> client = pubsub.Client()
179178
>>> topic = client.topic('topic_name')
180-
>>> subscription = pubusb.Subscription('subscription_name', topic,
181-
... push_endpoint=ENDPOINT)
179+
>>> subscription = topic.subscription('subscription_name',
180+
... push_endpoint=ENDPOINT)
182181
>>> subscription.modify_push_configuration(push_endpoint=None) # API request
183182

184183
List subscriptions for a topic:
@@ -209,7 +208,7 @@ Delete a subscription:
209208
>>> from gcloud import pubsub
210209
>>> client = pubsub.Client()
211210
>>> topic = client.topic('topic_name')
212-
>>> subscription = pubsub.Subscription('subscription_name', topic)
211+
>>> subscription = topic.subscription('subscription_name')
213212
>>> subscription.delete() # API request
214213

215214

@@ -223,7 +222,7 @@ Fetch pending messages for a pull subscription:
223222
>>> from gcloud import pubsub
224223
>>> client = pubsub.Client()
225224
>>> topic = client.topic('topic_name')
226-
>>> subscription = pubsub.Subscription('subscription_name', topic)
225+
>>> subscription = topic.subscription('subscription_name')
227226
>>> with topic.batch() as batch:
228227
... batch.publish('this is the first message_payload')
229228
... batch.publish('this is the second message_payload',
@@ -252,7 +251,7 @@ Fetch a limited number of pending messages for a pull subscription:
252251
>>> from gcloud import pubsub
253252
>>> client = pubsub.Client()
254253
>>> topic = client.topic('topic_name')
255-
>>> subscription = pubsub.Subscription('subscription_name', topic)
254+
>>> subscription = topic.subscription('subscription_name')
256255
>>> with topic.batch() as batch:
257256
... batch.publish('this is the first message_payload')
258257
... batch.publish('this is the second message_payload',
@@ -268,7 +267,7 @@ Fetch messages for a pull subscription without blocking (none pending):
268267
>>> from gcloud import pubsub
269268
>>> client = pubsub.Client()
270269
>>> topic = client.topic('topic_name')
271-
>>> subscription = pubsub.Subscription('subscription_name', topic)
270+
>>> subscription = topic.subscription('subscription_name')
272271
>>> received = subscription.pull(max_messages=1) # API request
273272
>>> messages = [recv[1] for recv in received]
274273
>>> [message.id for message in messages]

gcloud/pubsub/_helpers.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright 2015 Google Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Helper functions for shared behavior."""
16+
17+
18+
def topic_name_from_path(path, project):
19+
"""Validate a topic URI path and get the topic name.
20+
21+
:type path: string
22+
:param path: URI path for a topic API request.
23+
24+
:type project: string
25+
:param project: The project associated with the request. It is
26+
included for validation purposes.
27+
28+
:rtype: string
29+
:returns: Topic name parsed from ``path``.
30+
:raises: :class:`ValueError` if the ``path`` is ill-formed or if
31+
the project from the ``path`` does not agree with the
32+
``project`` passed in.
33+
"""
34+
# PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME)
35+
path_parts = path.split('/')
36+
if (len(path_parts) != 4 or path_parts[0] != 'projects' or
37+
path_parts[2] != 'topics'):
38+
raise ValueError('Expected path to be of the form '
39+
'projects/{project}/topics/{topic_name}')
40+
if (len(path_parts) != 4 or path_parts[0] != 'projects' or
41+
path_parts[2] != 'topics' or path_parts[1] != project):
42+
raise ValueError('Project from client should agree with '
43+
'project from resource.')
44+
45+
return path_parts[3]

gcloud/pubsub/subscription.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
"""Define API Subscriptions."""
1616

1717
from gcloud.exceptions import NotFound
18+
from gcloud.pubsub._helpers import topic_name_from_path
1819
from gcloud.pubsub.message import Message
19-
from gcloud.pubsub.topic import Topic
2020

2121

2222
class Subscription(object):
@@ -65,11 +65,13 @@ def from_api_repr(cls, resource, client, topics=None):
6565
"""
6666
if topics is None:
6767
topics = {}
68-
t_name = resource['topic']
69-
topic = topics.get(t_name)
68+
topic_path = resource['topic']
69+
topic = topics.get(topic_path)
7070
if topic is None:
71-
topic = topics[t_name] = Topic.from_api_repr({'name': t_name},
72-
client)
71+
# NOTE: This duplicates behavior from Topic.from_api_repr to avoid
72+
# an import cycle.
73+
topic_name = topic_name_from_path(topic_path, client.project)
74+
topic = topics[topic_path] = client.topic(topic_name)
7375
_, _, _, name = resource['name'].split('/')
7476
ack_deadline = resource.get('ackDeadlineSeconds')
7577
push_config = resource.get('pushConfig', {})

gcloud/pubsub/test__helpers.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright 2015 Google Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest2
16+
17+
18+
class Test_topic_name_from_path(unittest2.TestCase):
19+
20+
def _callFUT(self, path, project):
21+
from gcloud.pubsub._helpers import topic_name_from_path
22+
return topic_name_from_path(path, project)
23+
24+
def test_invalid_path_length(self):
25+
PATH = 'projects/foo'
26+
PROJECT = None
27+
self.assertRaises(ValueError, self._callFUT, PATH, PROJECT)
28+
29+
def test_invalid_path_format(self):
30+
TOPIC_NAME = 'TOPIC_NAME'
31+
PROJECT = 'PROJECT'
32+
PATH = 'foo/%s/bar/%s' % (PROJECT, TOPIC_NAME)
33+
self.assertRaises(ValueError, self._callFUT, PATH, PROJECT)
34+
35+
def test_invalid_project(self):
36+
TOPIC_NAME = 'TOPIC_NAME'
37+
PROJECT1 = 'PROJECT1'
38+
PROJECT2 = 'PROJECT2'
39+
PATH = 'projects/%s/topics/%s' % (PROJECT1, TOPIC_NAME)
40+
self.assertRaises(ValueError, self._callFUT, PATH, PROJECT2)
41+
42+
def test_valid_data(self):
43+
TOPIC_NAME = 'TOPIC_NAME'
44+
PROJECT = 'PROJECT'
45+
PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME)
46+
topic_name = self._callFUT(PATH, PROJECT)
47+
self.assertEqual(topic_name, TOPIC_NAME)

gcloud/pubsub/test_subscription.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,7 @@ class _Client(object):
518518
def __init__(self, project, connection=None):
519519
self.project = project
520520
self.connection = connection
521+
522+
def topic(self, name, timestamp_messages=False):
523+
from gcloud.pubsub.topic import Topic
524+
return Topic(name, client=self, timestamp_messages=timestamp_messages)

gcloud/pubsub/test_topic.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,20 @@ def test_delete_w_alternate_client(self):
331331
self.assertEqual(req['method'], 'DELETE')
332332
self.assertEqual(req['path'], '/%s' % PATH)
333333

334+
def test_subscription(self):
335+
from gcloud.pubsub.subscription import Subscription
336+
TOPIC_NAME = 'topic_name'
337+
PROJECT = 'PROJECT'
338+
CLIENT = _Client(project=PROJECT)
339+
topic = self._makeOne(TOPIC_NAME,
340+
client=CLIENT)
341+
342+
SUBSCRIPTION_NAME = 'subscription_name'
343+
subscription = topic.subscription(SUBSCRIPTION_NAME)
344+
self.assertTrue(isinstance(subscription, Subscription))
345+
self.assertEqual(subscription.name, SUBSCRIPTION_NAME)
346+
self.assertTrue(subscription.topic is topic)
347+
334348

335349
class TestBatch(unittest2.TestCase):
336350

gcloud/pubsub/topic.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
from gcloud._helpers import _RFC3339_MICROS
2121
from gcloud.exceptions import NotFound
22+
from gcloud.pubsub._helpers import topic_name_from_path
23+
from gcloud.pubsub.subscription import Subscription
2224

2325
_NOW = datetime.datetime.utcnow
2426

@@ -48,6 +50,24 @@ def __init__(self, name, client, timestamp_messages=False):
4850
self._client = client
4951
self.timestamp_messages = timestamp_messages
5052

53+
def subscription(self, name, ack_deadline=None, push_endpoint=None):
54+
"""Creates a subscription bound to the current topic.
55+
56+
:type name: string
57+
:param name: the name of the subscription
58+
59+
:type ack_deadline: int
60+
:param ack_deadline: the deadline (in seconds) by which messages pulled
61+
from the back-end must be acknowledged.
62+
63+
:type push_endpoint: string
64+
:param push_endpoint: URL to which messages will be pushed by the
65+
back-end. If not set, the application must pull
66+
messages.
67+
"""
68+
return Subscription(name, self, ack_deadline=ack_deadline,
69+
push_endpoint=push_endpoint)
70+
5171
@classmethod
5272
def from_api_repr(cls, resource, client):
5373
"""Factory: construct a topic given its API representation
@@ -65,11 +85,8 @@ def from_api_repr(cls, resource, client):
6585
project from the resource does not agree with the project
6686
from the client.
6787
"""
68-
_, project, _, name = resource['name'].split('/')
69-
if client.project != project:
70-
raise ValueError('Project from clientshould agree with '
71-
'project from resource.')
72-
return cls(name, client=client)
88+
topic_name = topic_name_from_path(resource['name'], client.project)
89+
return cls(topic_name, client=client)
7390

7491
@property
7592
def project(self):

system_tests/pubsub.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
from gcloud import _helpers
2020
from gcloud import pubsub
21-
from gcloud.pubsub.subscription import Subscription
2221

2322

2423
_helpers._PROJECT_ENV_VAR_NAME = 'GCLOUD_TESTS_PROJECT_ID'
@@ -68,7 +67,7 @@ def test_create_subscription(self):
6867
topic.create()
6968
self.to_delete.append(topic)
7069
SUBSCRIPTION_NAME = 'subscribing-now'
71-
subscription = Subscription(SUBSCRIPTION_NAME, topic)
70+
subscription = topic.subscription(SUBSCRIPTION_NAME)
7271
self.assertFalse(subscription.exists())
7372
subscription.create()
7473
self.to_delete.append(subscription)
@@ -88,7 +87,7 @@ def test_list_subscriptions(self):
8887
'newest%d' % (1000 * time.time(),),
8988
]
9089
for subscription_name in subscriptions_to_create:
91-
subscription = Subscription(subscription_name, topic)
90+
subscription = topic.subscription(subscription_name)
9291
subscription.create()
9392
self.to_delete.append(subscription)
9493

@@ -106,7 +105,7 @@ def test_message_pull_mode_e2e(self):
106105
topic.create()
107106
self.to_delete.append(topic)
108107
SUBSCRIPTION_NAME = 'subscribing-now'
109-
subscription = Subscription(SUBSCRIPTION_NAME, topic)
108+
subscription = topic.subscription(SUBSCRIPTION_NAME)
110109
self.assertFalse(subscription.exists())
111110
subscription.create()
112111
self.to_delete.append(subscription)

0 commit comments

Comments
 (0)