8000 Add 'Bucket.list_notifications' API wrapper. (#3990) · googleapis/google-cloud-python@352aeed · GitHub
[go: up one dir, main page]

Skip to content

Commit 352aeed

Browse files
committed
Add 'Bucket.list_notifications' API wrapper. (#3990)
Toward #3956.
1 parent 2293eda commit 352aeed

File tree

4 files changed

+214
-5
lines changed

4 files changed

+214
-5
lines changed

storage/google/cloud/storage/bucket.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,26 @@ def _item_to_blob(iterator, item):
7777
return blob
7878

7979

80+
def _item_to_notification(iterator, item):
81+
"""Convert a JSON blob to the native object.
82+
83+
.. note::
84+
85+
This assumes that the ``bucket`` attribute has been
86+
added to the iterator after being created.
87+
88+
:type iterator: :class:`~google.api.core.page_iterator.Iterator`
89+
:param iterator: The iterator that has retrieved the item.
90+
91+
:type item: dict
92+
:param item: An item to be converted to a blob.
93+
94+
:rtype: :class:`.BucketNotification`
95+
:returns: The next notification being iterated.
96+
"""
97+
return BucketNotification.from_api_repr(item, bucket=iterator.bucket)
98+
99+
80100
class Bucket(_PropertyMixin):
81101
"""A class representing a Bucket on Cloud Storage.
82102
@@ -168,10 +188,9 @@ def notification(self, topic_name,
168188
payload_format=None):
169189
"""Factory: create a notification resource for the bucket.
170190
171-
See: :class:`google.cloud.storage.notification.BucketNotification`
172-
for parameters.
191+
See: :class:`.BucketNotification` for parameters.
173192
174-
:rtype: :class:`google.cloud.storage.notification.BucketNotification`
193+
:rtype: :class:`.BucketNotification`
175194
"""
176195
return BucketNotification(
177196
self, topic_name,
@@ -405,6 +424,30 @@ def list_blobs(self, max_results=None, page_token=None, prefix=None,
405424
iterator.prefixes = set()
406425
return iterator
407426

427+
def list_notifications(self, client=None):
428+
"""List Pub / Sub notifications for this bucket.
429+
430+
See:
431+
https://cloud.google.com/storage/docs/json_api/v1/notifications/list
432+
433+
:type client: :class:`~google.cloud.storage.client.Client` or
434+
``NoneType``
435+
:param client: Optional. The client to use. If not passed, falls back
436+
to the ``client`` stored on the current bucket.
437+
438+
:rtype: list of :class:`.BucketNotification`
439+
:returns: notification instances
440+
"""
441+
client = self._require_client(client)
442+
path = self.path + '/notificationConfigs'
443+
iterator = page_iterator.HTTPIterator(
444+
client=client,
445+
api_request=client._connection.api_request,
446+
path=path,
447+
item_to_value=_item_to_notification)
448+
iterator.bucket = self
449+
return iterator
450+
408451
def delete(self, force=False, client=None):
409452
"""Delete this bucket.
410453

storage/google/cloud/storage/notification.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
"""Support for bucket notification resources."""
1616

17+
import re
18+
1719
from google.api.core.exceptions import NotFound
1820

1921

@@ -25,7 +27,12 @@
2527
JSON_API_V1_PAYLOAD_FORMAT = 'JSON_API_V1'
2628
NONE_PAYLOAD_FORMAT = 'NONE'
2729

28-
_TOPIC_REF = '//pubsub.googleapis.com/projects/{}/topics/{}'
30+
_TOPIC_REF_FMT = '//pubsub.googleapis.com/projects/{}/topics/{}'
31+
_PROJECT_PATTERN = r'(?P<project>[a-z]+-[a-z]+-\d+)'
32+
_TOPIC_NAME_PATTERN = r'(?P<name>[A-Za-z](\w|[-_.~+%])+)'
33+
_TOPIC_REF_PATTERN = _TOPIC_REF_FMT.format(
34+
_PROJECT_PATTERN, _TOPIC_NAME_PATTERN)
35+
_TOPIC_REF_RE = re.compile(_TOPIC_REF_PATTERN)
2936

3037

3138
class BucketNotification(object):
@@ -85,6 +92,44 @@ def __init__(self, bucket, topic_name,
8592
if payload_format is not None:
8693
self._properties['payload_format'] = payload_format
8794

95+
@classmethod
96+
def from_api_repr(cls, resource, bucket):
97+
"""Construct an instance from the JSON repr returned by the server.
98+
99+
See: https://cloud.google.com/storage/docs/json_api/v1/notifications
100+
101+
:type resource: dict
102+
:param resource: JSON repr of the notification
103+
104+
:type bucket: :class:`google.cloud.storage.bucket.Bucket`
105+
:param bucket: Bucket to which the notification is bound.
106+
107+
:rtype: :class:`BucketNotification`
108+
:returns: the new notification instance
109+
:raises ValueError:
110+
if resource is missing 'topic' key, or if it is not formatted
111+
per the spec documented in
112+
https://cloud.google.com/storage/docs/json_api/v1/notifications/insert#topic
113+
"""
114+
topic_path = resource.get('topic')
115+
if topic_path is None:
116+
raise ValueError('Resource has no topic')
117+
118+
match = _TOPIC_REF_RE.match(topic_path)
119+
if match is None:
120+
raise ValueError(
121+
'Resource has invalid topic: {}; see {}'.format(
122+
topic_path,
123+
'https://cloud.google.com/storage/docs/json_api/v1/'
124+
'notifications/insert#topic'))
125+
126+
name = match.group('name')
127+
project = match.group('project')
128+
instance = cls(bucket, name, topic_project=project)
129+
instance._properties = resource
130+
131+
return instance
132+
88133
@property
89134
def bucket(self):
90135
"""Bucket to which the notification is bound."""
@@ -191,7 +236,7 @@ def create(self, client=None):
191236

192237
path = '/b/{}/notificationConfigs'.format(self.bucket.name)
193238
properties = self._properties.copy()
194-
properties['topic'] = _TOPIC_REF.format(
239+
properties['topic'] = _TOPIC_REF_FMT.format(
195240
self.topic_project, self.topic_name)
196241
self._properties = client._connection.api_request(
197242
method='POST',

storage/tests/unit/test_bucket.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,54 @@ def test_list_blobs(self):
389389
self.assertEqual(kw['path'], '/b/%s/o' % NAME)
390390
self.assertEqual(kw['query_params'], {'projection': 'noAcl'})
391391

392+
def test_list_notifications(self):
393+
from google.cloud.storage.notification import BucketNotification
394+
from google.cloud.storage.notification import _TOPIC_REF_FMT
395+
396+
NAME = 'name'
397+
398+
topic_refs = [
399+
('my-project-123', 'topic-1'),
400+
('other-project-456', 'topic-2'),
401+
]
402+
403+
resources = [{
404+
'topic': _TOPIC_REF_FMT.format(*topic_refs[0]),
405+
'id': '1',
406+
'etag': 'DEADBEEF',
407+
'selfLink': 'https://example.com/notification/1',
408+
}, {
409+
'topic': _TOPIC_REF_FMT.format(*topic_refs[1]),
410+
'id': '2',
411+
'etag': 'FACECABB',
412+
'selfLink': 'https://example.com/notification/2',
413+
}]
414+
connection = _Connection({'items': resources})
415+
client = _Client(connection)
416+
bucket = self._make_one(client=client, name=NAME)
417+
418+
notifications = list(bucket.list_notifications())
419+
420+
self.assertEqual(len(notifications), len(resources))
421+
for notification, resource, topic_ref in zip(
422+
notifications, resources, topic_refs):
423+
self.assertIsInstance(notification, BucketNotification)
424+
self.assertEqual(notification.topic_project, topic_ref[0])
425+
self.assertEqual(notification.topic_name, topic_ref[1])
426+
self.assertEqual(notification.notification_id, resource['id'])
427+
self.assertEqual(notification.etag, resource['etag'])
428+
self.assertEqual(notification.self_link, resource['selfLink'])
429+
self.assertEqual(
430+
notification.custom_attributes,
431+
resource.get('custom_attributes'))
432+
self.assertEqual(
433+
notification.event_types, resource.get('event_types'))
434+
self.assertEqual(
435+
notification.blob_name_prefix,
436+
resource.get('blob_name_prefix'))
437+
self.assertEqual(
438+
notification.payload_format, resource.get('payload_format'))
439+
392440
def test_delete_miss(self):
393441
from google.cloud.exceptions import NotFound
394442

storage/tests/unit/test_notification.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,79 @@ def test_ctor_explicit(self):
111111
self.assertEqual(
112112
notification.payload_format, self.payload_format())
113113

114+
def test_from_api_repr_no_topic(self):
115+
klass = self._get_target_class()
116+
client = self._make_client()
117+
bucket = self._make_bucket(client)
118+
resource = {}
119+
120+
with self.assertRaises(ValueError):
121+
klass.from_api_repr(resource, bucket=bucket)
122+
123+
def test_from_api_repr_invalid_topic(self):
124+
klass = self._get_target_class()
125+
client = self._make_client()
126+
bucket = self._make_bucket(client)
127+
resource = {
128+
'topic': '@#$%',
129+
}
130+
131+
with self.assertRaises(ValueError):
132+
klass.from_api_repr(resource, bucket=bucket)
133+
134+
def test_from_api_repr_minimal(self):
135+
klass = self._get_target_class()
136+
client = self._make_client()
137+
bucket = self._make_bucket(client)
138+
resource = {
139+
'topic': self.TOPIC_REF,
140+
F90C 'id': self.NOTIFICATION_ID,
141+
'etag': self.ETAG,
142+
'selfLink': self.SELF_LINK,
143+
}
144+
145+
notification = klass.from_api_repr(resource, bucket=bucket)
146+
147+
self.assertIs(notification.bucket, bucket)
148+
self.assertEqual(notification.topic_name, self.TOPIC_NAME)
149+
self.assertEqual(notification.topic_project, self.BUCKET_PROJECT)
150+
self.assertIsNone(notification.custom_attributes)
151+
self.assertIsNone(notification.event_types)
152+
self.assertIsNone(notification.blob_name_prefix)
153+
self.assertIsNone(notification.payload_format)
154+
self.assertEqual(notification.etag, self.ETAG)
155+
self.assertEqual(notification.self_link, self.SELF_LINK)
156+
157+
def test_from_api_repr_explicit(self):
158+
klass = self._get_target_class()
159+
client = self._make_client()
160+
bucket = self._make_bucket(client)
161+
resource = {
162+
'topic': self.TOPIC_ALT_REF,
163+
'custom_attributes': self.CUSTOM_ATTRIBUTES,
164+
'event_types': self.event_types(),
165+
'blob_name_prefix': self.BLOB_NAME_PREFIX,
166+
'payload_format': self.payload_format(),
167+
'id': self.NOTIFICATION_ID,
168+
'etag': self.ETAG,
169+
'selfLink': self.SELF_LINK,
170+
}
171+
172+
notification = klass.from_api_repr(resource, bucket=bucket)
173+
174+
self.assertIs(notification.bucket, bucket)
175+
self.assertEqual(notification.topic_name, self.TOPIC_NAME)
176+
self.assertEqual(notification.topic_project, self.TOPIC_ALT_PROJECT)
177+
self.assertEqual(
178+
notification.custom_attributes, self.CUSTOM_ATTRIBUTES)
179+
self.assertEqual(notification.event_types, self.event_types())
180+
self.assertEqual(notification.blob_name_prefix, self.BLOB_NAME_PREFIX)
181+
self.assertEqual(
182+
notification.payload_format, self.payload_format())
183+
self.assertEqual(notification.notification_id, self.NOTIFICATION_ID)
184+
self.assertEqual(notification.etag, self.ETAG)
185+
self.assertEqual(notification.self_link, self.SELF_LINK)
186+
114187
def test_notification_id(self):
115188
client = self._make_client()
116189
bucket = self._make_bucket(client)

0 commit comments

Comments
 (0)
0