Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'Bucket.list_notifications' API wrapper. #3990

Merged
merged 3 commits into from
Sep 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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