diff --git a/storage/google/cloud/storage/bucket.py b/storage/google/cloud/storage/bucket.py index 88ac69566e46..b280ecd55e30 100644 --- a/storage/google/cloud/storage/bucket.py +++ b/storage/google/cloud/storage/bucket.py @@ -35,6 +35,8 @@ from google.cloud.storage.acl import DefaultObjectACL from google.cloud.storage.blob import Blob from google.cloud.storage.blob import _get_encryption_headers +from google.cloud.storage.notification import BucketNotification +from google.cloud.storage.notification import NONE_PAYLOAD_FORMAT def _blobs_page_start(iterator, page, response): @@ -76,6 +78,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. @@ -159,6 +181,27 @@ def blob(self, blob_name, chunk_size=None, encryption_key=None): return Blob(name=blob_name, bucket=self, chunk_size=chunk_size, encryption_key=encryption_key) + def notification(self, topic_name, + topic_project=None, + custom_attributes=None, + event_types=None, + blob_name_prefix=None, + payload_format=NONE_PAYLOAD_FORMAT): + """Factory: create a notification resource for the bucket. + + See: :class:`.BucketNotification` for parameters. + + :rtype: :class:`.BucketNotification` + """ + return BucketNotification( + self, topic_name, + topic_project=topic_project, + custom_attributes=custom_attributes, + event_types=event_types, + blob_name_prefix=blob_name_prefix, + payload_format=payload_format, + ) + def exists(self, client=None): """Determines whether or not this bucket exists. @@ -382,6 +425,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. diff --git a/storage/google/cloud/storage/notification.py b/storage/google/cloud/storage/notification.py new file mode 100644 index 000000000000..8ee287bf5fcf --- /dev/null +++ b/storage/google/cloud/storage/notification.py @@ -0,0 +1,313 @@ +# Copyright 2017 Google Inc. +# +# 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. + +"""Support for bucket notification resources.""" + +import re + +from google.api.core.exceptions import NotFound + + +OBJECT_FINALIZE_EVENT_TYPE = 'OBJECT_FINALIZE' +OBJECT_METADATA_UPDATE_EVENT_TYPE = 'OBJECT_METADATA_UPDATE' +OBJECT_DELETE_EVENT_TYPE = 'OBJECT_DELETE' +OBJECT_ARCHIVE_EVENT_TYPE = 'OBJECT_ARCHIVE' + +JSON_API_V1_PAYLOAD_FORMAT = 'JSON_API_V1' +NONE_PAYLOAD_FORMAT = 'NONE' + +_TOPIC_REF_FMT = '//pubsub.googleapis.com/projects/{}/topics/{}' +_PROJECT_PATTERN = r'(?P[a-z]+-[a-z]+-\d+)' +_TOPIC_NAME_PATTERN = r'(?P[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): + """Represent a single notification resource for a bucket. + + See: https://cloud.google.com/storage/docs/json_api/v1/notifications + + :type bucket: :class:`google.cloud.storage.bucket.Bucket` + :param bucket: Bucket to which the notification is bound. + + :type topic_name: str + :param topic_name: Topic name to which notifications are published. + + :type topic_project: str + :param topic_project: + (Optional) project ID of topic to which notifications are published. + If not passed, uses the project ID of the bucket's client. + + :type custom_attributes: dict + :param custom_attributes: + (Optional) additional attributes passed with notification events. + + :type event_types: list(str) + :param event_types: + (Optional) event types for which notificatin events are published. + + :type blob_name_prefix: str + :param blob_name_prefix: + (Optional) prefix of blob names for which notification events are + published.. + + :type payload_format: str + :param payload_format: + (Optional) format of payload for notification events. + """ + def __init__(self, bucket, topic_name, + topic_project=None, custom_attributes=None, event_types=None, + blob_name_prefix=None, payload_format=NONE_PAYLOAD_FORMAT): + self._bucket = bucket + self._topic_name = topic_name + + if topic_project is None: + topic_project = bucket.client.project + self._topic_project = topic_project + + self._properties = {} + + if custom_attributes is not None: + self._properties['custom_attributes'] = custom_attributes + + if event_types is not None: + self._properties['event_types'] = event_types + + if blob_name_prefix is not None: + self._properties['object_name_prefix'] = blob_name_prefix + + 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.""" + return self._bucket + + @property + def topic_name(self): + """Topic name to which notifications are published.""" + return self._topic_name + + @property + def topic_project(self): + """Project ID of topic to which notifications are published. + """ + return self._topic_project + + @property + def custom_attributes(self): + """Custom attributes passed with notification events. + """ + return self._properties.get('custom_attributes') + + @property + def event_types(self): + """Event types for which notification events are published. + """ + return self._properties.get('event_types') + + @property + def blob_name_prefix(self): + """Prefix of blob names for which notification events are published. + """ + return self._properties.get('object_name_prefix') + + @property + def payload_format(self): + """Format of payload of notification events.""" + return self._properties.get('payload_format') + + @property + def notification_id(self): + """Server-set ID of notification resource.""" + return self._properties.get('id') + + @property + def etag(self): + """Server-set ETag of notification resource.""" + return self._properties.get('etag') + + @property + def self_link(self): + """Server-set ETag of notification resource.""" + return self._properties.get('selfLink') + + @property + def client(self): + """The client bound to this notfication.""" + return self.bucket.client + + @property + def path(self): + """The URL path for this notification.""" + return '/b/{}/notificationConfigs/{}'.format( + self.bucket.name, self.notification_id) + + def _require_client(self, client): + """Check client or verify over-ride. + + :type client: :class:`~google.cloud.storage.client.Client` or + ``NoneType`` + :param client: the client to use. + + :rtype: :class:`google.cloud.storage.client.Client` + :returns: The client passed in or the bucket's client. + """ + if client is None: + client = self.client + return client + + def _set_properties(self, response): + """Helper for :meth:`reload`. + + :type response: dict + :param response: resource mapping from server + """ + self._properties.clear() + self._properties.update(response) + + def create(self, client=None): + """API wrapper: create the notification. + + See: + https://cloud.google.com/storage/docs/json_api/v1/notifications/insert + + :type client: :class:`~google.cloud.storage.client.Client` + :param client: (Optional) the client to use. If not passed, falls back + to the ``client`` stored on the notification's bucket. + """ + if self.notification_id is not None: + raise ValueError("Notification already exists w/ id: {}".format( + self.notification_id)) + + client = self._require_client(client) + + path = '/b/{}/notificationConfigs'.format(self.bucket.name) + properties = self._properties.copy() + properties['topic'] = _TOPIC_REF_FMT.format( + self.topic_project, self.topic_name) + self._properties = client._connection.api_request( + method='POST', + path=path, + data=properties, + ) + + def exists(self, client=None): + """Test whether this notification exists. + + See: + https://cloud.google.com/storage/docs/json_api/v1/notifications/get + + :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: bool + :returns: True, if the notification exists, else False. + :raises ValueError: if the notification has no ID. + """ + if self.notification_id is None: + raise ValueError("Notification not intialized by server") + + client = self._require_client(client) + try: + client._connection.api_request(method='GET', path=self.path) + except NotFound: + return False + else: + return True + + def reload(self, client=None): + """Update this notification from the server configuration. + + See: + https://cloud.google.com/storage/docs/json_api/v1/notifications/get + + :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: bool + :returns: True, if the notification exists, else False. + :raises ValueError: if the notification has no ID. + """ + if self.notification_id is None: + raise ValueError("Notification not intialized by server") + + client = self._require_client(client) + response = client._connection.api_request(method='GET', path=self.path) + self._set_properties(response) + + def delete(self, client=None): + """Delete this notification. + + See: + https://cloud.google.com/storage/docs/json_api/v1/notifications/delete + + :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. + + :raises: :class:`google.api.core.exceptions.NotFound`: + if the notification does not exist. + :raises ValueError: if the notification has no ID. + """ + if self.notification_id is None: + raise ValueError("Notification not intialized by server") + + client = self._require_client(client) + client._connection.api_request(method='DELETE', path=self.path) diff --git a/storage/nox.py b/storage/nox.py index 18ccf81aaff2..5f831fb88ac8 100644 --- a/storage/nox.py +++ b/storage/nox.py @@ -71,10 +71,11 @@ def system_tests(session, python_version): # virutalenv's dist-packages. session.install('mock', 'pytest', *LOCAL_DEPS) session.install('../test_utils/') - session.install('.') + session.install('../pubsub') + session.install('-e', '.') # Run py.test against the system tests. - session.run('py.test', '--quiet', 'tests/system.py') + session.run('py.test', '--quiet', 'tests/system.py', *session.posargs) @nox.session diff --git a/storage/tests/system.py b/storage/tests/system.py index e51cfcaeccb2..c9ea16638b95 100644 --- a/storage/tests/system.py +++ b/storage/tests/system.py @@ -550,3 +550,103 @@ def test_rewrite_rotate_encryption_key(self): self.assertEqual(total, len(source_data)) self.assertEqual(dest.download_as_string(), source_data) + + +class TestStorageNotificationCRUD(unittest.TestCase): + + topic = None + TOPIC_NAME = 'notification' + unique_resource_id('-') + CUSTOM_ATTRIBUTES = { + 'attr1': 'value1', + 'attr2': 'value2', + } + BLOB_NAME_PREFIX = 'blob-name-prefix/' + + @property + def topic_path(self): + return 'projects/{}/topics/{}'.format( + Config.CLIENT.project, self.TOPIC_NAME) + + def _intialize_topic(self): + try: + from google.cloud.pubsub_v1 import PublisherClient + except ImportError: + raise unittest.SkipTest("Cannot import pubsub") + self.publisher_client = PublisherClient() + retry_429(self.publisher_client.create_topic)(self.topic_path) + policy = self.publisher_client.get_iam_policy(self.topic_path) + binding = policy.bindings.add() + binding.role = 'roles/pubsub.publisher' + binding.members.append( + 'serviceAccount:{}' + '@gs-project-accounts.iam.gserviceaccount.com'.format( + Config.CLIENT.project)) + self.publisher_client.set_iam_policy(self.topic_path, policy) + + + def setUp(self): + self.case_buckets_to_delete = [] + self._intialize_topic() + + def tearDown(self): + retry_429(self.publisher_client.delete_topic)(self.topic_path) + with Config.CLIENT.batch(): + for bucket_name in self.case_buckets_to_delete: + bucket = Config.CLIENT.bucket(bucket_name) + retry_429(bucket.delete)() + + @staticmethod + def event_types(): + from google.cloud.storage.notification import ( + OBJECT_FINALIZE_EVENT_TYPE, + OBJECT_DELETE_EVENT_TYPE) + + return [OBJECT_FINALIZE_EVENT_TYPE, OBJECT_DELETE_EVENT_TYPE] + + @staticmethod + def payload_format(): + from google.cloud.storage.notification import ( + JSON_API_V1_PAYLOAD_FORMAT) + + return JSON_API_V1_PAYLOAD_FORMAT + + def test_notification_minimal(self): + new_bucket_name = 'notification-minimal' + unique_resource_id('-') + bucket = retry_429(Config.CLIENT.create_bucket)(new_bucket_name) + self.case_buckets_to_delete.append(new_bucket_name) + self.assertEqual(list(bucket.list_notifications()), []) + notification = bucket.notification(self.TOPIC_NAME) + retry_429(notification.create)() + try: + self.assertTrue(notification.exists()) + self.assertIsNotNone(notification.notification_id) + notifications = list(bucket.list_notifications()) + self.assertEqual(len(notifications), 1) + self.assertEqual(notifications[0].topic_name, self.TOPIC_NAME) + finally: + notification.delete() + + def test_notification_explicit(self): + new_bucket_name = 'notification-explicit' + unique_resource_id('-') + bucket = retry_429(Config.CLIENT.create_bucket)(new_bucket_name) + self.case_buckets_to_delete.append(new_bucket_name) + notification = bucket.notification( + self.TOPIC_NAME, + custom_attributes=self.CUSTOM_ATTRIBUTES, + event_types=self.event_types(), + blob_name_prefix=self.BLOB_NAME_PREFIX, + payload_format=self.payload_format(), + ) + retry_429(notification.create)() + try: + self.assertTrue(notification.exists()) + self.assertIsNotNone(notification.notification_id) + 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()) + finally: + notification.delete() diff --git a/storage/tests/unit/test_bucket.py b/storage/tests/unit/test_bucket.py index 1fd2da128756..31e5f817e1aa 100644 --- a/storage/tests/unit/test_bucket.py +++ b/storage/tests/unit/test_bucket.py @@ -72,6 +72,64 @@ def test_blob(self): self.assertEqual(blob.chunk_size, CHUNK_SIZE) self.assertEqual(blob._encryption_key, KEY) + def test_notification_defaults(self): + from google.cloud.storage.notification import BucketNotification + from google.cloud.storage.notification import NONE_PAYLOAD_FORMAT + + PROJECT = 'PROJECT' + BUCKET_NAME = 'BUCKET_NAME' + TOPIC_NAME = 'TOPIC_NAME' + client = _Client(_Connection(), project=PROJECT) + bucket = self._make_one(client, name=BUCKET_NAME) + + notification = bucket.notification(TOPIC_NAME) + + self.assertIsInstance(notification, BucketNotification) + self.assertIs(notification.bucket, bucket) + self.assertEqual(notification.topic_project, PROJECT) + self.assertIsNone(notification.custom_attributes) + self.assertIsNone(notification.event_types) + self.assertIsNone(notification.blob_name_prefix) + self.assertEqual(notification.payload_format, NONE_PAYLOAD_FORMAT) + + def test_notification_explicit(self): + from google.cloud.storage.notification import ( + BucketNotification, + OBJECT_FINALIZE_EVENT_TYPE, + OBJECT_DELETE_EVENT_TYPE, + JSON_API_V1_PAYLOAD_FORMAT) + + PROJECT = 'PROJECT' + BUCKET_NAME = 'BUCKET_NAME' + TOPIC_NAME = 'TOPIC_NAME' + TOPIC_ALT_PROJECT = 'topic-project-456' + CUSTOM_ATTRIBUTES = { + 'attr1': 'value1', + 'attr2': 'value2', + } + EVENT_TYPES = [OBJECT_FINALIZE_EVENT_TYPE, OBJECT_DELETE_EVENT_TYPE] + BLOB_NAME_PREFIX = 'blob-name-prefix/' + client = _Client(_Connection(), project=PROJECT) + bucket = self._make_one(client, name=BUCKET_NAME) + + notification = bucket.notification( + TOPIC_NAME, + topic_project=TOPIC_ALT_PROJECT, + custom_attributes=CUSTOM_ATTRIBUTES, + event_types=EVENT_TYPES, + blob_name_prefix=BLOB_NAME_PREFIX, + payload_format=JSON_API_V1_PAYLOAD_FORMAT, + ) + + self.assertIsInstance(notification, BucketNotification) + self.assertIs(notification.bucket, bucket) + self.assertEqual(notification.topic_project, TOPIC_ALT_PROJECT) + self.assertEqual(notification.custom_attributes, CUSTOM_ATTRIBUTES) + self.assertEqual(notification.event_types, EVENT_TYPES) + self.assertEqual(notification.blob_name_prefix, BLOB_NAME_PREFIX) + self.assertEqual( + notification.payload_format, JSON_API_V1_PAYLOAD_FORMAT) + def test_bucket_name_value(self): bucket_name = 'testing123' mixin = self._make_one(name=bucket_name) @@ -332,6 +390,58 @@ 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 + from google.cloud.storage.notification import ( + JSON_API_V1_PAYLOAD_FORMAT, NONE_PAYLOAD_FORMAT) + + 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', + 'payload_format': NONE_PAYLOAD_FORMAT, + }, { + 'topic': _TOPIC_REF_FMT.format(*topic_refs[1]), + 'id': '2', + 'etag': 'FACECABB', + 'selfLink': 'https://example.com/notification/2', + 'payload_format': JSON_API_V1_PAYLOAD_FORMAT, + }] + 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 diff --git a/storage/tests/unit/test_notification.py b/storage/tests/unit/test_notification.py new file mode 100644 index 000000000000..2832c217c045 --- /dev/null +++ b/storage/tests/unit/test_notification.py @@ -0,0 +1,469 @@ +# Copyright 2017 Google Inc. +# +# 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 unittest + +import mock + + +class TestBucketNotification(unittest.TestCase): + + BUCKET_NAME = 'test-bucket' + BUCKET_PROJECT = 'bucket-project-123' + TOPIC_NAME = 'test-topic' + TOPIC_ALT_PROJECT = 'topic-project-456' + TOPIC_REF_FMT = '//pubsub.googleapis.com/projects/{}/topics/{}' + TOPIC_REF = TOPIC_REF_FMT.format(BUCKET_PROJECT, TOPIC_NAME) + TOPIC_ALT_REF = TOPIC_REF_FMT.format(TOPIC_ALT_PROJECT, TOPIC_NAME) + CUSTOM_ATTRIBUTES = { + 'attr1': 'value1', + 'attr2': 'value2', + } + BLOB_NAME_PREFIX = 'blob-name-prefix/' + NOTIFICATION_ID = '123' + SELF_LINK = 'https://example.com/notification/123' + ETAG = 'DEADBEEF' + CREATE_PATH = '/b/{}/notificationConfigs'.format(BUCKET_NAME) + NOTIFICATION_PATH = '/b/{}/notificationConfigs/{}'.format( + BUCKET_NAME, NOTIFICATION_ID) + + @staticmethod + def event_types(): + from google.cloud.storage.notification import ( + OBJECT_FINALIZE_EVENT_TYPE, + OBJECT_DELETE_EVENT_TYPE) + + return [OBJECT_FINALIZE_EVENT_TYPE, OBJECT_DELETE_EVENT_TYPE] + + @staticmethod + def payload_format(): + from google.cloud.storage.notification import ( + JSON_API_V1_PAYLOAD_FORMAT) + + return JSON_API_V1_PAYLOAD_FORMAT + + @staticmethod + def _get_target_class(): + from google.cloud.storage.notification import BucketNotification + + return BucketNotification + + def _make_one(self, *args, **kw): + return self._get_target_class()(*args, **kw) + + def _make_client(self, project=BUCKET_PROJECT): + from google.cloud.storage.client import Client + + return mock.Mock(project=project, spec=Client) + + def _make_bucket(self, client, name=BUCKET_NAME): + bucket = mock.Mock(spec=['client', 'name']) + bucket.client= client + bucket.name = name + return bucket + + def test_ctor_defaults(self): + from google.cloud.storage.notification import NONE_PAYLOAD_FORMAT + + client = self._make_client() + bucket = self._make_bucket(client) + + notification = self._make_one( + bucket, self.TOPIC_NAME) + + 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.assertEqual(notification.payload_format, NONE_PAYLOAD_FORMAT) + + def test_ctor_explicit(self): + client = self._make_client() + bucket = self._make_bucket(client) + + notification = self._make_one( + bucket, self.TOPIC_NAME, + topic_project=self.TOPIC_ALT_PROJECT, + custom_attributes=self.CUSTOM_ATTRIBUTES, + event_types=self.event_types(), + blob_name_prefix=self.BLOB_NAME_PREFIX, + payload_format=self.payload_format(), + ) + + 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()) + + 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): + from google.cloud.storage.notification import NONE_PAYLOAD_FORMAT + + 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, + 'payload_format': NONE_PAYLOAD_FORMAT, + } + + 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.assertEqual(notification.payload_format, NONE_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(), + 'object_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) + + notification = self._make_one( + bucket, self.TOPIC_NAME) + + self.assertIsNone(notification.notification_id) + + notification._properties['id'] = self.NOTIFICATION_ID + self.assertEqual(notification.notification_id, self.NOTIFICATION_ID) + + def test_etag(self): + client = self._make_client() + bucket = self._make_bucket(client) + + notification = self._make_one( + bucket, self.TOPIC_NAME) + + self.assertIsNone(notification.etag) + + notification._properties['etag'] = self.ETAG + self.assertEqual(notification.etag, self.ETAG) + + def test_self_link(self): + client = self._make_client() + bucket = self._make_bucket(client) + + notification = self._make_one( + bucket, self.TOPIC_NAME) + + self.assertIsNone(notification.self_link) + + notification._properties['selfLink'] = self.SELF_LINK + self.assertEqual(notification.self_link, self.SELF_LINK) + + def test_create_w_existing_notification_id(self): + client = self._make_client() + bucket = self._make_bucket(client) + notification = self._make_one( + bucket, self.TOPIC_NAME) + notification._properties['id'] = self.NOTIFICATION_ID + + with self.assertRaises(ValueError): + notification.create() + + def test_create_w_defaults(self): + from google.cloud.storage.notification import NONE_PAYLOAD_FORMAT + + client = self._make_client() + bucket = self._make_bucket(client) + notification = self._make_one( + bucket, self.TOPIC_NAME) + api_request = client._connection.api_request + api_request.return_value = { + 'topic': self.TOPIC_REF, + 'id': self.NOTIFICATION_ID, + 'etag': self.ETAG, + 'selfLink': self.SELF_LINK, + 'payload_format': NONE_PAYLOAD_FORMAT, + } + + notification.create() + + self.assertEqual(notification.notification_id, self.NOTIFICATION_ID) + self.assertEqual(notification.etag, self.ETAG) + self.assertEqual(notification.self_link, self.SELF_LINK) + self.assertIsNone(notification.custom_attributes) + self.assertIsNone(notification.event_types) + self.assertIsNone(notification.blob_name_prefix) + self.assertEqual(notification.payload_format, NONE_PAYLOAD_FORMAT) + + data = { + 'topic': self.TOPIC_REF, + 'payload_format': NONE_PAYLOAD_FORMAT, + } + api_request.assert_called_once_with( + method='POST', + path=self.CREATE_PATH, + data=data, + ) + + def test_create_w_explicit_client(self): + client = self._make_client() + alt_client = self._make_client() + bucket = self._make_bucket(client) + notification = self._make_one( + bucket, self.TOPIC_NAME, + topic_project=self.TOPIC_ALT_PROJECT, + custom_attributes=self.CUSTOM_ATTRIBUTES, + event_types=self.event_types(), + blob_name_prefix=self.BLOB_NAME_PREFIX, + payload_format=self.payload_format(), + ) + api_request = alt_client._connection.api_request + api_request.return_value = { + 'topic': self.TOPIC_ALT_REF, + 'custom_attributes': self.CUSTOM_ATTRIBUTES, + 'event_types': self.event_types(), + 'object_name_prefix': self.BLOB_NAME_PREFIX, + 'payload_format': self.payload_format(), + 'id': self.NOTIFICATION_ID, + 'etag': self.ETAG, + 'selfLink': self.SELF_LINK, + } + + notification.create(client=alt_client) + + 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) + + data = { + 'topic': self.TOPIC_ALT_REF, + 'custom_attributes': self.CUSTOM_ATTRIBUTES, + 'event_types': self.event_types(), + 'object_name_prefix': self.BLOB_NAME_PREFIX, + 'payload_format': self.payload_format(), + } + api_request.assert_called_once_with( + method='POST', + path=self.CREATE_PATH, + data=data, + ) + + def test_exists_wo_notification_id(self): + client = self._make_client() + bucket = self._make_bucket(client) + notification = self._make_one( + bucket, self.TOPIC_NAME) + + with self.assertRaises(ValueError): + notification.exists() + + def test_exists_miss(self): + from google.cloud.exceptions import NotFound + + client = self._make_client() + bucket = self._make_bucket(client) + notification = self._make_one(bucket, self.TOPIC_NAME) + notification._properties['id'] = self.NOTIFICATION_ID + api_request = client._connection.api_request + api_request.side_effect = NotFound('testing') + + self.assertFalse(notification.exists()) + + api_request.assert_called_once_with( + method='GET', + path=self.NOTIFICATION_PATH, + ) + + def test_exists_hit(self): + client = self._make_client() + bucket = self._make_bucket(client) + alt_client = self._make_client() + notification = self._make_one(bucket, self.TOPIC_NAME) + notification._properties['id'] = self.NOTIFICATION_ID + api_request = client._connection.api_request + api_request.return_value = { + 'topic': self.TOPIC_REF, + 'id': self.NOTIFICATION_ID, + 'etag': self.ETAG, + 'selfLink': self.SELF_LINK, + } + + self.assertTrue(notification.exists(client=client)) + + api_request.assert_called_once_with( + method='GET', + path=self.NOTIFICATION_PATH, + ) + + def test_reload_wo_notification_id(self): + client = self._make_client() + bucket = self._make_bucket(client) + notification = self._make_one( + bucket, self.TOPIC_NAME) + + with self.assertRaises(ValueError): + notification.reload() + + def test_reload_miss(self): + from google.cloud.exceptions import NotFound + + client = self._make_client() + bucket = self._make_bucket(client) + notification = self._make_one(bucket, self.TOPIC_NAME) + notification._properties['id'] = self.NOTIFICATION_ID + api_request = client._connection.api_request + api_request.side_effect = NotFound('testing') + + with self.assertRaises(NotFound): + notification.reload() + + api_request.assert_called_once_with( + method='GET', + path=self.NOTIFICATION_PATH, + ) + + def test_reload_hit(self): + from google.cloud.storage.notification import NONE_PAYLOAD_FORMAT + + client = self._make_client() + bucket = self._make_bucket(client) + alt_client = self._make_client() + notification = self._make_one(bucket, self.TOPIC_NAME) + notification._properties['id'] = self.NOTIFICATION_ID + api_request = client._connection.api_request + api_request.return_value = { + 'topic': self.TOPIC_REF, + 'id': self.NOTIFICATION_ID, + 'etag': self.ETAG, + 'selfLink': self.SELF_LINK, + 'payload_format': NONE_PAYLOAD_FORMAT, + } + + notification.reload(client=client) + + self.assertEqual(notification.etag, self.ETAG) + self.assertEqual(notification.self_link, self.SELF_LINK) + self.assertIsNone(notification.custom_attributes) + self.assertIsNone(notification.event_types) + self.assertIsNone(notification.blob_name_prefix) + self.assertEqual(notification.payload_format, NONE_PAYLOAD_FORMAT) + + api_request.assert_called_once_with( + method='GET', + path=self.NOTIFICATION_PATH, + ) + + def test_delete_wo_notification_id(self): + client = self._make_client() + bucket = self._make_bucket(client) + notification = self._make_one( + bucket, self.TOPIC_NAME) + + with self.assertRaises(ValueError): + notification.delete() + + def test_delete_miss(self): + from google.cloud.exceptions import NotFound + + client = self._make_client() + bucket = self._make_bucket(client) + notification = self._make_one(bucket, self.TOPIC_NAME) + notification._properties['id'] = self.NOTIFICATION_ID + api_request = client._connection.api_request + api_request.side_effect = NotFound('testing') + + with self.assertRaises(NotFound): + notification.delete() + + api_request.assert_called_once_with( + method='DELETE', + path=self.NOTIFICATION_PATH, + ) + + def test_delete_hit(self): + client = self._make_client() + bucket = self._make_bucket(client) + alt_client = self._make_client() + notification = self._make_one(bucket, self.TOPIC_NAME) + notification._properties['id'] = self.NOTIFICATION_ID + api_request = client._connection.api_request + api_request.return_value = None + + notification.delete(client=client) + + api_request.assert_called_once_with( + method='DELETE', + path=self.NOTIFICATION_PATH, + )