diff --git a/docs/API.md b/docs/API.md
index 94017a1d0..9da0e22e0 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -40,6 +40,9 @@ s3Client = Minio('s3.amazonaws.com',
|[`list_incomplete_uploads`](#list_incomplete_uploads) | [`fput_object`](#fput_object) | |
| [`get_bucket_policy`](#get_bucket_policy) |[`fget_object`](#fget_object) | |
| [`set_bucket_policy`](#set_bucket_policy) | [`get_partial_object`](#get_partial_object) | |
+| [`get_bucket_notification`](#get_bucket_notification) | |
+| [`set_bucket_notification`](#set_bucket_notification) | |
+| [`remove_all_bucket_notifications`](#remove_all_bucket_notifications) | |
## 1. Constructor
@@ -232,7 +235,7 @@ __Parameters__
|Param |Type |Description |
|:---|:---|:---|
-|``bucketname`` | _string_|Name of the bucket.|
+|``bucket_name`` | _string_|Name of the bucket.|
|``prefix`` |_string_ |The prefix of the incomplete objects uploaded should be listed. |
|``recursive`` |_bool_ |``True`` indicates recursive style listing and ``False`` indicates directory style listing delimited by '/'. Optional default is ``False``. |
@@ -270,7 +273,7 @@ __Parameters__
|Param |Type |Description |
|:---|:---|:---|
-|``bucketname`` | _string_ |Name of the bucket.|
+|``bucket_name`` | _string_ |Name of the bucket.|
|``prefix`` |_string_ |The prefix of objects to get current policy. |
__Return Value__
@@ -302,7 +305,7 @@ __Parameters__
|Param |Type |Description |
|:---|:---|:---|
-|``bucketname`` | _string_ |Name of the bucket.|
+|``bucket_name`` | _string_ |Name of the bucket.|
|``prefix`` |_string_ |The prefix of objects to get current policy. |
|``Policy`` | _minio.policy.Policy_ |Policy enum. Policy.READ_ONLY,Policy.WRITE_ONLY,Policy.READ_WRITE or Policy.NONE. |
@@ -319,6 +322,192 @@ minioClient.set_bucket_policy('mybucket',
```
+
+### get_bucket_notification(bucket_name)
+
+Fetch the notifications configuration on a bucket.
+
+__Parameters__
+
+|Param |Type |Description |
+|:---|:---|:---|
+|``bucket_name`` | _string_ |Name of the bucket.|
+
+__Return Value__
+
+|Param |Type |Description |
+|:---|:---|:---|
+|``notification`` | _dict_ | If there is no notification configuration, an empty dictionary is returned. Otherwise it has the same structure as the argument to set_bucket_notification |
+
+__Example__
+
+
+```py
+
+# Get the notifications configuration for a bucket.
+notification = minioClient.get_bucket_notification('mybucket')
+# If no notification is present on the bucket:
+# notification == {}
+
+```
+
+
+### set_bucket_notification(bucket_name, notification)
+
+Set notification configuration on a bucket.
+
+__Parameters__
+
+|Param |Type |Description |
+|:---|:---|:---|
+|``bucket_name`` | _string_ |Name of the bucket.|
+|``notification`` | _dict_ |Non-empty dictionary with the structure specified below.|
+
+The `notification` argument has the following structure:
+
+* (dict) --
+ * __TopicConfigurations__ (list) -- Optional list of service
+ configuration items specifying AWS SNS Topics as the target of the
+ notification.
+ * __QueueConfigurations__ (list) -- Optional list of service
+ configuration items specifying AWS SQS Queues as the target of the
+ notification.
+ * __CloudFunctionconfigurations__ (list) -- Optional list of service
+ configuration items specifying AWS Lambda Cloud functions as the
+ target of the notification.
+
+At least one of the above items needs to be specified in the
+`notification` argument.
+
+The "service configuration item" alluded to above has the following structure:
+
+* (dict) --
+ * __Id__ (string) -- Optional Id for the configuration item. If not
+ specified, it is auto-generated by the server.
+ * __Arn__ (string) -- Specifies the particular Topic/Queue/Cloud
+ Function identifier.
+ * __Events__ (list) -- A non-empty list of event-type strings from:
+ _'s3:ReducedRedundancyLostObject'_,
+ _'s3:ObjectCreated:*'_,
+ _'s3:ObjectCreated:Put'_,
+ _'s3:ObjectCreated:Post'_,
+ _'s3:ObjectCreated:Copy'_,
+ _'s3:ObjectCreated:CompleteMultipartUpload'_,
+ _'s3:ObjectRemoved:*'_,
+ _'s3:ObjectRemoved:Delete'_,
+ _'s3:ObjectRemoved:DeleteMarkerCreated'_
+ * __Filter__ (dict) -- An optional dictionary container of object
+ key name based filter rules.
+ * __Key__ (dict) -- Dictionary container of object key name prefix
+ and suffix filtering rules.
+ * __FilterRules__ (list) -- A list of containers that specify
+ the criteria for the filter rule.
+ * (dict) -- A dictionary container of key value pairs that
+ specify a single filter rule.
+ * __Name__ (string) -- Object key name with value 'prefix'
+ or 'suffix'.
+ * __Value__ (string) -- Specify the value of the
+ prefix/suffix to which the rule applies.
+
+
+There is no return value. If there are errors from the target
+server/service, a `ResponseError` is thrown. If there are validation
+errors, `InvalidArgumentError` or `TypeError` may be thrown. The input
+configuration cannot be empty - to delete the notification
+configuration on a bucket, use the `remove_all_bucket_notifications()`
+API.
+
+__Example__
+
+
+```py
+
+notification = {
+ 'QueueConfigurations': [
+ {
+ 'Id': '1',
+ 'Arn': 'arn1',
+ 'Events': ['s3:ObjectCreated:PutObject'],
+ 'Filter': {
+ 'Key': {
+ 'FilterRules': [
+ {
+ 'Name': 'prefix',
+ 'Value': 'abc'
+ }
+ ]
+ }
+ }
+ }
+ ],
+ 'TopicConfigurations': [
+ {
+ 'Arn': 'arn2',
+ 'Events': ['s3:ObjectCreated:PostObject'],
+ 'Filter': {
+ 'Key': {
+ 'FilterRules': [
+ {
+ 'Name': 'suffix',
+ 'Value': '.jpg'
+ }
+ ]
+ }
+ }
+ }
+ ],
+ 'CloudFunctionConfigurations': [
+ {
+ 'Arn': 'arn3',
+ 'Events': ['s3:ObjectCreated:DeleteObject'],
+ 'Filter': {
+ 'Key': {
+ 'FilterRules': [
+ {
+ 'Name': 'suffix',
+ 'Value': '.jpg'
+ }
+ ]
+ }
+ }
+ }
+ ]
+}
+
+try:
+ minioClient.set_bucket_notification('mybucket', notification)
+except ResponseError:
+ # handle error response from service.
+ pass
+except (ArgumentError, TypeError):
+ # should happen only during development. Fix the notification argument
+ pass
+```
+
+
+### remove_all_bucket_notifications(bucket_name)
+
+Remove all notifications configured on the bucket.
+
+__Parameters__
+
+|Param |Type |Description |
+|:---|:---|:---|
+|``bucket_name`` | _string_ |Name of the bucket.|
+
+There is no returned value. A `ResponseError` exception is thrown if
+the operation did not complete successfully.
+
+__Example__
+
+
+```py
+
+# Get the notifications configuration for a bucket.
+minioClient.remove_all_bucket_notifications('mybucket')
+
+```
+
## 3. Object operations
### get_object(bucket_name, object_name)
diff --git a/minio/api.py b/minio/api.py
index 51c96430e..ba78a43be 100644
--- a/minio/api.py
+++ b/minio/api.py
@@ -53,12 +53,14 @@
parse_list_multipart_uploads,
parse_new_multipart_upload,
parse_location_constraint,
- parse_multipart_upload_result)
+ parse_multipart_upload_result,
+ parse_get_bucket_notification)
from .helpers import (get_target_url, is_non_empty_string,
is_valid_endpoint,
get_sha256, encode_to_base64, get_md5,
optimal_part_info, encode_to_hex,
is_valid_bucket_name, parts_manager,
+ is_valid_bucket_notification_config,
mkdir_p, dump_http)
from .helpers import (MAX_MULTIPART_OBJECT_SIZE,
MIN_OBJECT_SIZE)
@@ -66,7 +68,8 @@
generate_credential_string,
post_presign_signature, _SIGN_V4_ALGORITHM)
from .xml_marshal import (xml_marshal_bucket_constraint,
- xml_marshal_complete_multipart_upload)
+ xml_marshal_complete_multipart_upload,
+ xml_marshal_bucket_notifications)
from .limited_reader import LimitedReader
from . import policy
@@ -381,6 +384,71 @@ def set_bucket_policy(self, bucket_name, prefix, policy_access):
body=content,
content_sha256=content_sha256_hex)
+ def get_bucket_notification(self, bucket_name):
+ """
+ Get notifications configured for the given bucket.
+
+ :param bucket_name: Bucket name.
+ """
+ is_valid_bucket_name(bucket_name)
+
+ response = self._url_open(
+ "GET",
+ bucket_name=bucket_name,
+ query={"notification": ""},
+ headers={}
+ )
+ data = response.read().decode('utf-8')
+ return parse_get_bucket_notification(data)
+
+ def set_bucket_notification(self, bucket_name, notifications):
+ """
+ Set the given notifications on the bucket.
+
+ :param bucket_name: Bucket name.
+ :param notifications: Notifications structure
+ """
+ is_valid_bucket_name(bucket_name)
+ is_valid_bucket_notification_config(notifications)
+
+ content = xml_marshal_bucket_notifications(notifications)
+ headers = {
+ 'Content-Length': str(len(content)),
+ 'Content-MD5': encode_to_base64(get_md5(content))
+ }
+ content_sha256_hex = encode_to_hex(get_sha256(content))
+ self._url_open(
+ 'PUT',
+ bucket_name=bucket_name,
+ query={"notification": ""},
+ headers=headers,
+ body=content,
+ content_sha256=content_sha256_hex
+ )
+
+ def remove_all_bucket_notifications(self, bucket_name):
+ """
+ Remove all notifications configured on the bucket.'
+
+ :param bucket_name: Bucket name.
+ """
+ is_valid_bucket_name(bucket_name)
+
+ content_bytes = xml_marshal_bucket_notifications({})
+ headers = {
+ 'Content-Length': str(len(content_bytes)),
+ 'Content-MD5': encode_to_base64(get_md5(content_bytes))
+ }
+ content_sha256_hex = encode_to_hex(get_sha256(content_bytes))
+ self._url_open(
+ 'PUT',
+ bucket_name=bucket_name,
+ query={"notification": ""},
+ headers=headers,
+ body=content_bytes,
+ content_sha256=content_sha256_hex
+ )
+
def _get_upload_id(self, bucket_name, object_name, content_type):
"""
Get previously uploaded upload id for object name or initiate a request to
diff --git a/minio/helpers.py b/minio/helpers.py
index 1faac84b5..cab3bd232 100644
--- a/minio/helpers.py
+++ b/minio/helpers.py
@@ -379,6 +379,146 @@ def is_non_empty_string(input_string):
return True
+def is_valid_bucket_notification_config(notifications):
+ """
+ Validate the notifications config structure
+
+ :param notifications: Dictionary with specific structure.
+ :return: True if input is a valid bucket notifications structure.
+ Raise :exc:`InvalidArgumentError` otherwise.
+ """
+ # check if notifications is a dict.
+ if not isinstance(notifications, dict):
+ raise TypeError('notifications configuration must be a dictionary')
+
+ if len(notifications) == 0:
+ raise InvalidArgumentError(
+ 'notifications configuration may not be empty'
+ )
+
+ VALID_NOTIFICATION_KEYS = set([
+ "TopicConfigurations",
+ "QueueConfigurations",
+ "CloudFunctionConfigurations",
+ ])
+
+ VALID_SERVICE_CONFIG_KEYS = set([
+ 'Id',
+ 'Arn',
+ 'Events',
+ 'Filter',
+ ])
+
+ NOTIFICATION_EVENTS = set([
+ 's3:ReducedRedundancyLostObject',
+ 's3:ObjectCreated:*',
+ 's3:ObjectCreated:Put',
+ 's3:ObjectCreated:Post',
+ 's3:ObjectCreated:Copy',
+ 's3:ObjectCreated:CompleteMultipartUpload',
+ 's3:ObjectRemoved:*',
+ 's3:ObjectRemoved:Delete',
+ 's3:ObjectRemoved:DeleteMarkerCreated',
+ ])
+
+ for key, value in notifications.items():
+ # check if key names are valid
+ if key not in VALID_NOTIFICATION_KEYS:
+ raise InvalidArgumentError(
+ ('{} is an invalid key '
+ 'for notifications configuration').format(
+ key
+ )
+ )
+
+ # check if config values conform
+ # first check if value is a list
+ if not isinstance(value, list):
+ raise InvalidArgumentError(
+ ('The value for key "{}" in the notifications '
+ 'configuration must be a list.').format(key)
+ )
+ for service_config in value:
+ # check type matches
+ if not isinstance(service_config, dict):
+ raise InvalidArgumentError(
+ ('Each service configuration item for "{}" must be a '
+ 'dictionary').format(key)
+ )
+ # check keys are valid
+ for skey in service_config.keys():
+ if skey not in VALID_SERVICE_CONFIG_KEYS:
+ raise InvalidArgumentError(
+ ('{} is an invalid key for a service '
+ 'configuration item').format(
+ skey
+ )
+ )
+ # check for required keys
+ arn = service_config.get('Arn', '')
+ if arn == '':
+ raise InvalidArgumentError(
+ 'Arn key in service config must be present and has to be '
+ 'non-empty string'
+ )
+ events = service_config.get('Events', [])
+ if len(events) < 1:
+ raise InvalidArgumentError(
+ 'At least one event must be specified in a service config'
+ )
+ if not isinstance(events, list):
+ raise InvalidArgumentError(
+ '"Events" must be a list of strings in a service '
+ 'configuration'
+ )
+ # check if 'Id' key is present, it should be a string
+ if not isinstance(service_config.get('Id', ''), str):
+ raise InvalidArgumentError(
+ '"Id" key must be a string'
+ )
+ for event in events:
+ if event not in NOTIFICATION_EVENTS:
+ raise InvalidArgumentError(
+ '{} is not a valid event. Valid events are: {}'.format(
+ event, NOTIFICATION_EVENTS
+ )
+ )
+ if 'Filter' in service_config:
+ exception_msg = (
+ '{} - If a Filter key is given, it must be a '
+ 'dictionary, the dictionary must have the '
+ 'key "Key", and its value must be an object, with '
+ 'a key named "FilterRules" which must be a non-empty list.'
+ ).format(
+ service_config['Filter']
+ )
+ try:
+ filter_rules = service_config.get('Filter', {}).get(
+ 'Key', {}).get('FilterRules', [])
+ if not isinstance(filter_rules, list):
+ raise InvalidArgumentError(exception_msg)
+ if len(filter_rules) < 1:
+ raise InvalidArgumentError(exception_msg)
+ except AttributeError:
+ raise InvalidArgumentError(exception_msg)
+ for filter_rule in filter_rules:
+ try:
+ name = filter_rule['Name']
+ value = filter_rule['Value']
+ except KeyError:
+ raise InvalidArgumentError(
+ ('{} - a FilterRule dictionary must have "Name" '
+ 'and "Value" keys').format(filter_rule)
+ )
+ if name not in ['prefix', 'suffix']:
+ raise InvalidArgumentError(
+ ('{} - The "Name" key in a filter rule must be '
+ 'either "prefix" or "suffix"').format(name)
+ )
+
+ return True
+
+
def encode_object_name(object_name):
"""
URL encode input object name.
diff --git a/minio/parsers.py b/minio/parsers.py
index 76ee27a6f..317125820 100644
--- a/minio/parsers.py
+++ b/minio/parsers.py
@@ -38,6 +38,8 @@
from .compat import urldecode
from .definitions import (Object, Bucket, IncompleteUpload,
UploadPart, MultipartUploadResult)
+from .xml_marshal import (NOTIFICATIONS_ARN_FIELDNAME_MAP)
+
if hasattr(cElementTree, 'ParseError'):
_ETREE_EXCEPTIONS = (ParseError, AttributeError, ValueError, TypeError)
@@ -339,3 +341,72 @@ def _iso8601_to_localized_time(date_string):
parsed_date = datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S.%fZ')
localized_time = pytz.utc.localize(parsed_date)
return localized_time
+
+def parse_get_bucket_notification(data):
+ """
+ Parser for a get_bucket_notification response from S3.
+
+ :param data: Body of response from get_bucket_notification.
+ :return: Returns bucket notification configuration
+ """
+ try:
+ root = cElementTree.fromstring(data)
+ except _ETREE_EXCEPTIONS as error:
+ raise InvalidXMLError('"GetBucketNotificationResult" XML is not parsable. '
+ 'Message: {0}'.format(error.message))
+
+ # perhaps we could ignore this condition?
+ if root.tag != '{{{s3}}}NotificationConfiguration'.format(**_S3_NS):
+ raise InvalidXMLError('"GetBucketNotificationresult" XML root is '
+ 'invalid.')
+
+ notifications = _parse_add_notifying_service_config(
+ root, {},
+ 'TopicConfigurations', 'TopicConfiguration'
+ )
+ notifications = _parse_add_notifying_service_config(
+ root, notifications,
+ 'QueueConfigurations', 'QueueConfiguration'
+ )
+ notifications = _parse_add_notifying_service_config(
+ root, notifications,
+ 'CloudFunctionConfigurations', 'CloudFunctionConfiguration'
+ )
+
+ return notifications
+
+def _parse_add_notifying_service_config(data, notifications, service_key,
+ service_xml_tag):
+ config = []
+ stag = 's3:{}'.format(service_xml_tag)
+ for service in data.findall(stag, _S3_NS):
+ service_config = {}
+ service_config['Id'] = service.find('s3:Id', _S3_NS).text
+ arn_tag = 's3:{}'.format(
+ NOTIFICATIONS_ARN_FIELDNAME_MAP[service_xml_tag]
+ )
+ service_config['Arn'] = service.find(arn_tag, _S3_NS).text
+ service_config['Events'] = []
+ for event in service.findall('s3:Event', _S3_NS):
+ service_config['Events'].append(event.text)
+ xml_filter_rule = service.find('s3:Filter', _S3_NS)
+ if xml_filter_rule:
+ xml_filter_rules = xml_filter_rule.find(
+ 's3:S3Key', _S3_NS).findall('s3:FilterRule', _S3_NS)
+ filter_rules = []
+ for xml_filter_rule in xml_filter_rules:
+ filter_rules.append(
+ {
+ 'Name': xml_filter_rule.find('s3:Name', _S3_NS),
+ 'Value': xml_filter_rule.find('s3:Value', _S3_NS),
+ }
+ )
+ service_config['Filter'] = {
+ 'Key': {
+ 'FilterRules': filter_rules
+ }
+ }
+ config.append(service_config)
+ if config:
+ notifications[service_key] = config
+ return notifications
diff --git a/minio/xml_marshal.py b/minio/xml_marshal.py
index 2d7dbd96b..45495979e 100644
--- a/minio/xml_marshal.py
+++ b/minio/xml_marshal.py
@@ -34,8 +34,8 @@
def xml_marshal_bucket_constraint(region):
"""
- Marshal's bucket constraint based on *region*
-.
+ Marshal's bucket constraint based on *region*.
+
:param region: Region name of a given bucket.
:return: Marshalled XML data.
"""
@@ -50,7 +50,7 @@ def xml_marshal_bucket_constraint(region):
def xml_marshal_complete_multipart_upload(uploaded_parts):
"""
Marshal's complete multipart upload request based on *uploaded_parts*.
-.
+
:param uploaded_parts: List of all uploaded parts ordered in the
way they were uploaded.
:return: Marshalled XML data.
@@ -66,3 +66,134 @@ def xml_marshal_complete_multipart_upload(uploaded_parts):
s3_xml.ElementTree(root).write(data, encoding=None,
xml_declaration=False)
return data.getvalue()
+
+def xml_marshal_bucket_notifications(notifications):
+ """
+ Marshals the notifications structure for sending to S3 compatible storage
+
+ :param notifications: Dictionary with following structure:
+
+ {
+ 'TopicConfigurations': [
+ {
+ 'Id': 'string',
+ 'Arn': 'string',
+ 'Events': [
+ 's3:ReducedRedundancyLostObject'|'s3:ObjectCreated:*'|'s3:ObjectCreated:Put'|'s3:ObjectCreated:Post'|'s3:ObjectCreated:Copy'|'s3:ObjectCreated:CompleteMultipartUpload'|'s3:ObjectRemoved:*'|'s3:ObjectRemoved:Delete'|'s3:ObjectRemoved:DeleteMarkerCreated',
+ ],
+ 'Filter': {
+ 'Key': {
+ 'FilterRules': [
+ {
+ 'Name': 'prefix'|'suffix',
+ 'Value': 'string'
+ },
+ ]
+ }
+ }
+ },
+ ],
+ 'QueueConfigurations': [
+ {
+ 'Id': 'string',
+ 'Arn': 'string',
+ 'Events': [
+ 's3:ReducedRedundancyLostObject'|'s3:ObjectCreated:*'|'s3:ObjectCreated:Put'|'s3:ObjectCreated:Post'|'s3:ObjectCreated:Copy'|'s3:ObjectCreated:CompleteMultipartUpload'|'s3:ObjectRemoved:*'|'s3:ObjectRemoved:Delete'|'s3:ObjectRemoved:DeleteMarkerCreated',
+ ],
+ 'Filter': {
+ 'Key': {
+ 'FilterRules': [
+ {
+ 'Name': 'prefix'|'suffix',
+ 'Value': 'string'
+ },
+ ]
+ }
+ }
+ },
+ ],
+ 'CloudFunctionConfigurations': [
+ {
+ 'Id': 'string',
+ 'Arn': 'string',
+ 'Events': [
+ 's3:ReducedRedundancyLostObject'|'s3:ObjectCreated:*'|'s3:ObjectCreated:Put'|'s3:ObjectCreated:Post'|'s3:ObjectCreated:Copy'|'s3:ObjectCreated:CompleteMultipartUpload'|'s3:ObjectRemoved:*'|'s3:ObjectRemoved:Delete'|'s3:ObjectRemoved:DeleteMarkerCreated',
+ ],
+ 'Filter': {
+ 'Key': {
+ 'FilterRules': [
+ {
+ 'Name': 'prefix'|'suffix',
+ 'Value': 'string'
+ },
+ ]
+ }
+ }
+ },
+ ]
+ }
+
+ :return: Marshalled XML data
+ """
+ root = s3_xml.Element('NotificationConfiguration', {'xmlns': _S3_NAMESPACE})
+ _add_notification_config_to_xml(
+ root,
+ 'TopicConfiguration',
+ notifications.get('TopicConfigurations', [])
+ )
+ _add_notification_config_to_xml(
+ root,
+ 'QueueConfiguration',
+ notifications.get('QueueConfigurations', [])
+ )
+ _add_notification_config_to_xml(
+ root,
+ 'CloudFunctionConfiguration',
+ notifications.get('CloudFunctionConfigurations', [])
+ )
+
+ data = io.BytesIO()
+ s3_xml.ElementTree(root).write(data, encoding=None, xml_declaration=False)
+ return data.getvalue()
+
+NOTIFICATIONS_ARN_FIELDNAME_MAP = {
+ 'TopicConfiguration': 'Topic',
+ 'QueueConfiguration': 'Queue',
+ 'CloudFunctionConfiguration': 'CloudFunction',
+}
+
+def _add_notification_config_to_xml(node, element_name, configs):
+ """
+ Internal function that builds the XML sub-structure for a given
+ kind of notification configuration.
+
+ """
+ for config in configs:
+ config_node = s3_xml.SubElement(node, element_name)
+
+ if 'Id' in config:
+ id_node = s3_xml.SubElement(config_node, 'Id')
+ id_node.text = config['Id']
+
+ arn_node = s3_xml.SubElement(
+ config_node,
+ NOTIFICATIONS_ARN_FIELDNAME_MAP[element_name]
+ )
+ arn_node.text = config['Arn']
+
+ for event in config['Events']:
+ event_node = s3_xml.SubElement(config_node, 'Event')
+ event_node.text = event
+
+ filter_rules = config_node.get('Filter', {}).get(
+ 'Key', {}).get('FilterRules', [])
+ if filter_rules:
+ filter_node = s3_xml.SubElement(config_node, 'Filter')
+ s3key_node = s3_xml.SubElement(filter_node, 'S3Key')
+ for filter_rule in filter_rules:
+ filter_rule_node = s3_xml.SubElement(s3key_node, 'FilterRule')
+ name_node = s3_xml.SubElement(filter_rule_node, 'Name')
+ name_node.text = filter_rule['Name']
+ value_node = s3_xml.SubElement(filter_rule_node, 'Value')
+ value_node.text = filter_rule['Value']
+ return node
diff --git a/tests/unit/set_bucket_notification_test.py b/tests/unit/set_bucket_notification_test.py
new file mode 100644
index 000000000..3af451852
--- /dev/null
+++ b/tests/unit/set_bucket_notification_test.py
@@ -0,0 +1,377 @@
+# -*- coding: utf-8 -*-
+# Minio Python Library for Amazon S3 Compatible Cloud Storage, (C) 2015 Minio, 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 mock
+
+from unittest import TestCase
+from nose.tools import raises
+
+from minio import Minio
+from minio.api import _DEFAULT_USER_AGENT
+from minio.error import InvalidArgumentError
+
+from .minio_mocks import MockResponse, MockConnection
+
+class SetBucketNotificationTest(TestCase):
+ @raises(TypeError)
+ def test_notification_is_dict_1(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification('my-test-bucket', 'abc')
+
+ @raises(TypeError)
+ def test_notification_is_dict_2(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification('my-test-bucket', ['myconfig1'])
+
+ @raises(InvalidArgumentError)
+ def test_notification_config_is_nonempty(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {}
+ )
+
+ @raises(InvalidArgumentError)
+ def test_notification_config_has_valid_keys(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfiguration': [
+ {
+ 'Id': '1',
+ 'Arn': 'arn1',
+ 'Events': ['s3:ObjectCreated:*'],
+ }
+ ]
+ }
+ )
+
+ @raises(InvalidArgumentError)
+ def test_notification_config_arn_key_is_present(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Id': '1',
+ 'Events': ['s3:ObjectCreated:*'],
+ }
+ ]
+ }
+ )
+
+ @raises(InvalidArgumentError)
+ def test_notification_config_id_key_is_string(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Id': 1,
+ 'Arn': 'abc',
+ 'Events': ['s3:ObjectCreated:*'],
+ }
+ ]
+ }
+ )
+
+ @raises(InvalidArgumentError)
+ def test_notification_config_events_key_is_present(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Id': '1',
+ 'Arn': 'arn1',
+ }
+ ]
+ }
+ )
+
+ @raises(InvalidArgumentError)
+ def test_notification_config_event_values_are_valid(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Id': '1',
+ 'Arn': 'arn1',
+ 'Events': ['object_created']
+ }
+ ]
+ }
+ )
+
+ @mock.patch('urllib3.PoolManager')
+ def test_notification_config_id_key_is_optional(self, mock_connection):
+ mock_server = MockConnection()
+ mock_connection.return_value = mock_server
+ mock_server.mock_add_request(
+ MockResponse(
+ 'PUT',
+ 'https://localhost:9000/my-test-bucket/?notification=',
+ {
+ 'Content-MD5': 'f+TfVp/A4pNnI7S4S+MkFg==',
+ 'Content-Length': '196',
+ 'User-Agent': _DEFAULT_USER_AGENT,
+ },
+ 200, content=""
+ )
+ )
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Arn': 'arn1',
+ 'Events': ['s3:ObjectCreated:*'],
+ }
+ ]
+ }
+ )
+
+ @raises(InvalidArgumentError)
+ def test_notification_config_has_valid_event_names(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Id': '1',
+ 'Arn': 'arn1',
+ 'Events': ['object_created'],
+ }
+ ]
+ }
+ )
+
+ @raises(InvalidArgumentError)
+ def test_notification_config_filterspec_is_valid_1(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Id': '1',
+ 'Arn': 'arn1',
+ 'Events': ['s3:ObjectCreated:*'],
+ 'Filter': []
+ }
+ ]
+ }
+ )
+
+ @raises(InvalidArgumentError)
+ def test_notification_config_filterspec_is_valid_2(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Id': '1',
+ 'Arn': 'arn1',
+ 'Events': ['s3:ObjectCreated:*'],
+ 'Filter': {
+ 'S3Key': {
+ }
+ }
+ }
+ ]
+ }
+ )
+
+ @raises(InvalidArgumentError)
+ def test_notification_config_filterspec_is_valid_3(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Id': '1',
+ 'Arn': 'arn1',
+ 'Events': ['s3:ObjectCreated:*'],
+ 'Filter': {
+ 'Key': {
+ }
+ }
+ }
+ ]
+ }
+ )
+
+ @raises(InvalidArgumentError)
+ def test_notification_config_filterspec_is_valid_4(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Id': '1',
+ 'Arn': 'arn1',
+ 'Events': ['s3:ObjectCreated:*'],
+ 'Filter': {
+ 'Key': {
+ 'FilterRules': []
+ }
+ }
+ }
+ ]
+ }
+ )
+
+ @raises(InvalidArgumentError)
+ def test_notification_config_filterspec_is_valid_5(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Id': '1',
+ 'Arn': 'arn1',
+ 'Events': ['s3:ObjectCreated:*'],
+ 'Filter': {
+ 'Key': {
+ 'FilterRules': [
+ {
+ 'rule1': 'ab',
+ 'val1': 'abc'
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ )
+
+ @raises(InvalidArgumentError)
+ def test_notification_config_filterspec_is_valid_6(self):
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Id': '1',
+ 'Arn': 'arn1',
+ 'Events': ['s3:ObjectCreated:*'],
+ 'Filter': {
+ 'Key': {
+ 'FilterRules': [
+ {
+ 'Name': 'ab',
+ 'Value': 'abc'
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ )
+
+ @mock.patch('urllib3.PoolManager')
+ def test_notification_config_filterspec_is_valid_7(self, mock_connection):
+ mock_server = MockConnection()
+ mock_connection.return_value = mock_server
+ mock_server.mock_add_request(
+ MockResponse(
+ 'PUT',
+ 'https://localhost:9000/my-test-bucket/?notification=',
+ {
+ 'Content-MD5': 'AGCNfbD5OuiyIJFd+r67MA==',
+ 'User-Agent': _DEFAULT_USER_AGENT,
+ 'Content-Length': '206',
+ },
+ 200, content=""
+ )
+ )
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Id': '1',
+ 'Arn': 'arn1',
+ 'Events': ['s3:ObjectCreated:*'],
+ 'Filter': {
+ 'Key': {
+ 'FilterRules': [
+ {
+ 'Name': 'prefix',
+ 'Value': 'abc'
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ )
+
+ @mock.patch('urllib3.PoolManager')
+ def test_notification_config_filterspec_is_valid_8(self, mock_connection):
+ mock_server = MockConnection()
+ mock_connection.return_value = mock_server
+ mock_server.mock_add_request(
+ MockResponse(
+ 'PUT',
+ 'https://localhost:9000/my-test-bucket/?notification=',
+ {
+ 'Content-Length': '206',
+ 'Content-MD5': 'AGCNfbD5OuiyIJFd+r67MA==',
+ 'User-Agent': _DEFAULT_USER_AGENT,
+ },
+ 200, content=""
+ )
+ )
+ client = Minio('localhost:9000')
+ client.set_bucket_notification(
+ 'my-test-bucket',
+ {
+ 'QueueConfigurations': [
+ {
+ 'Id': '1',
+ 'Arn': 'arn1',
+ 'Events': ['s3:ObjectCreated:*'],
+ 'Filter': {
+ 'Key': {
+ 'FilterRules': [
+ {
+ 'Name': 'suffix',
+ 'Value': 'abc'
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ )