Skip to content

Commit

Permalink
Add 'Bucket.list_notifications' API wrapper. (#3990)
Browse files Browse the repository at this point in the history
Toward #3956.
  • Loading branch information
tseaver authored Sep 21, 2017
1 parent 7190c86 commit 9eb9a46
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 5 deletions.
49 changes: 46 additions & 3 deletions storage/google/cloud/storage/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,26 @@ def _item_to_blob(iterator, item):
return blob


def _item_to_notification(iterator, item):
"""Convert a JSON blob to the native object.
.. note::
This assumes that the ``bucket`` attribute has been
added to the iterator after being created.
:type iterator: :class:`~google.api.core.page_iterator.Iterator`
:param iterator: The iterator that has retrieved the item.
:type item: dict
:param item: An item to be converted to a blob.
:rtype: :class:`.BucketNotification`
:returns: The next notification being iterated.
"""
return BucketNotification.from_api_repr(item, bucket=iterator.bucket)


class Bucket(_PropertyMixin):
"""A class representing a Bucket on Cloud Storage.
Expand Down Expand Up @@ -168,10 +188,9 @@ def notification(self, topic_name,
payload_format=None):
"""Factory: create a notification resource for the bucket.
See: :class:`google.cloud.storage.notification.BucketNotification`
for parameters.
See: :class:`.BucketNotification` for parameters.
:rtype: :class:`google.cloud.storage.notification.BucketNotification`
:rtype: :class:`.BucketNotification`
"""
return BucketNotification(
self, topic_name,
Expand Down Expand Up @@ -405,6 +424,30 @@ def list_blobs(self, max_results=None, page_token=None, prefix=None,
iterator.prefixes = set()
return iterator

def list_notifications(self, client=None):
"""List Pub / Sub notifications for this bucket.
See:
https://cloud.google.com/storage/docs/json_api/v1/notifications/list
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: Optional. The client to use. If not passed, falls back
to the ``client`` stored on the current bucket.
:rtype: list of :class:`.BucketNotification`
:returns: notification instances
"""
client = self._require_client(client)
path = self.path + '/notificationConfigs'
iterator = page_iterator.HTTPIterator(
client=client,
api_request=client._connection.api_request,
path=path,
item_to_value=_item_to_notification)
iterator.bucket = self
return iterator

def delete(self, force=False, client=None):
"""Delete this bucket.
Expand Down
49 changes: 47 additions & 2 deletions storage/google/cloud/storage/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

"""Support for bucket notification resources."""

import re

from google.api.core.exceptions import NotFound


Expand All @@ -25,7 +27,12 @@
JSON_API_V1_PAYLOAD_FORMAT = 'JSON_API_V1'
NONE_PAYLOAD_FORMAT = 'NONE'

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


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

@classmethod
def from_api_repr(cls, resource, bucket):
"""Construct an instance from the JSON repr returned by the server.
See: https://cloud.google.com/storage/docs/json_api/v1/notifications
:type resource: dict
:param resource: JSON repr of the notification
:type bucket: :class:`google.cloud.storage.bucket.Bucket`
:param bucket: Bucket to which the notification is bound.
:rtype: :class:`BucketNotification`
:returns: the new notification instance
:raises ValueError:
if resource is missing 'topic' key, or if it is not formatted
per the spec documented in
https://cloud.google.com/storage/docs/json_api/v1/notifications/insert#topic
"""
topic_path = resource.get('topic')
if topic_path is None:
raise ValueError('Resource has no topic')

match = _TOPIC_REF_RE.match(topic_path)
if match is None:
raise ValueError(
'Resource has invalid topic: {}; see {}'.format(
topic_path,
'https://cloud.google.com/storage/docs/json_api/v1/'
'notifications/insert#topic'))

name = match.group('name')
project = match.group('project')
instance = cls(bucket, name, topic_project=project)
instance._properties = resource

return instance

@property
def bucket(self):
"""Bucket to which the notification is bound."""
Expand Down Expand Up @@ -191,7 +236,7 @@ def create(self, client=None):

path = '/b/{}/notificationConfigs'.format(self.bucket.name)
properties = self._properties.copy()
properties['topic'] = _TOPIC_REF.format(
properties['topic'] = _TOPIC_REF_FMT.format(
self.topic_project, self.topic_name)
self._properties = client._connection.api_request(
method='POST',
Expand Down
48 changes: 48 additions & 0 deletions storage/tests/unit/test_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,54 @@ def test_list_blobs(self):
self.assertEqual(kw['path'], '/b/%s/o' % NAME)
self.assertEqual(kw['query_params'], {'projection': 'noAcl'})

def test_list_notifications(self):
from google.cloud.storage.notification import BucketNotification
from google.cloud.storage.notification import _TOPIC_REF_FMT

NAME = 'name'

topic_refs = [
('my-project-123', 'topic-1'),
('other-project-456', 'topic-2'),
]

resources = [{
'topic': _TOPIC_REF_FMT.format(*topic_refs[0]),
'id': '1',
'etag': 'DEADBEEF',
'selfLink': 'https://example.com/notification/1',
}, {
'topic': _TOPIC_REF_FMT.format(*topic_refs[1]),
'id': '2',
'etag': 'FACECABB',
'selfLink': 'https://example.com/notification/2',
}]
connection = _Connection({'items': resources})
client = _Client(connection)
bucket = self._make_one(client=client, name=NAME)

notifications = list(bucket.list_notifications())

self.assertEqual(len(notifications), len(resources))
for notification, resource, topic_ref in zip(
notifications, resources, topic_refs):
self.assertIsInstance(notification, BucketNotification)
self.assertEqual(notification.topic_project, topic_ref[0])
self.assertEqual(notification.topic_name, topic_ref[1])
self.assertEqual(notification.notification_id, resource['id'])
self.assertEqual(notification.etag, resource['etag'])
self.assertEqual(notification.self_link, resource['selfLink'])
self.assertEqual(
notification.custom_attributes,
resource.get('custom_attributes'))
self.assertEqual(
notification.event_types, resource.get('event_types'))
self.assertEqual(
notification.blob_name_prefix,
resource.get('blob_name_prefix'))
self.assertEqual(
notification.payload_format, resource.get('payload_format'))

def test_delete_miss(self):
from google.cloud.exceptions import NotFound

Expand Down
73 changes: 73 additions & 0 deletions storage/tests/unit/test_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,79 @@ def test_ctor_explicit(self):
self.assertEqual(
notification.payload_format, self.payload_format())

def test_from_api_repr_no_topic(self):
klass = self._get_target_class()
client = self._make_client()
bucket = self._make_bucket(client)
resource = {}

with self.assertRaises(ValueError):
klass.from_api_repr(resource, bucket=bucket)

def test_from_api_repr_invalid_topic(self):
klass = self._get_target_class()
client = self._make_client()
bucket = self._make_bucket(client)
resource = {
'topic': '@#$%',
}

with self.assertRaises(ValueError):
klass.from_api_repr(resource, bucket=bucket)

def test_from_api_repr_minimal(self):
klass = self._get_target_class()
client = self._make_client()
bucket = self._make_bucket(client)
resource = {
'topic': self.TOPIC_REF,
'id': self.NOTIFICATION_ID,
'etag': self.ETAG,
'selfLink': self.SELF_LINK,
}

notification = klass.from_api_repr(resource, bucket=bucket)

self.assertIs(notification.bucket, bucket)
self.assertEqual(notification.topic_name, self.TOPIC_NAME)
self.assertEqual(notification.topic_project, self.BUCKET_PROJECT)
self.assertIsNone(notification.custom_attributes)
self.assertIsNone(notification.event_types)
self.assertIsNone(notification.blob_name_prefix)
self.assertIsNone(notification.payload_format)
self.assertEqual(notification.etag, self.ETAG)
self.assertEqual(notification.self_link, self.SELF_LINK)

def test_from_api_repr_explicit(self):
klass = self._get_target_class()
client = self._make_client()
bucket = self._make_bucket(client)
resource = {
'topic': self.TOPIC_ALT_REF,
'custom_attributes': self.CUSTOM_ATTRIBUTES,
'event_types': self.event_types(),
'blob_name_prefix': self.BLOB_NAME_PREFIX,
'payload_format': self.payload_format(),
'id': self.NOTIFICATION_ID,
'etag': self.ETAG,
'selfLink': self.SELF_LINK,
}

notification = klass.from_api_repr(resource, bucket=bucket)

self.assertIs(notification.bucket, bucket)
self.assertEqual(notification.topic_name, self.TOPIC_NAME)
self.assertEqual(notification.topic_project, self.TOPIC_ALT_PROJECT)
self.assertEqual(
notification.custom_attributes, self.CUSTOM_ATTRIBUTES)
self.assertEqual(notification.event_types, self.event_types())
self.assertEqual(notification.blob_name_prefix, self.BLOB_NAME_PREFIX)
self.assertEqual(
notification.payload_format, self.payload_format())
self.assertEqual(notification.notification_id, self.NOTIFICATION_ID)
self.assertEqual(notification.etag, self.ETAG)
self.assertEqual(notification.self_link, self.SELF_LINK)

def test_notification_id(self):
client = self._make_client()
bucket = self._make_bucket(client)
Expand Down

0 comments on commit 9eb9a46

Please sign in to comment.