Repository: minio/minio-py
Pull request #1010 (2020-10-21 05:52:00)

Refactor XML handling of _complete_multipart_upload() API


Patch:
diff --git a/docs/API.md b/docs/API.md
index 5780a7d..4fef1e0 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -47,8 +47,8 @@ s3Client = Minio(
 | [`set_bucket_lifecycle`](#set_bucket_lifecycle)             | [`enable_object_legal_hold`](#enable_object_legal_hold)         |                                                   |
 | [`delete_bucket_tags`](#delete_bucket_tags)                 | [`disable_object_legal_hold`](#disable_object_legal_hold)       |                                                   |
 | [`get_bucket_tags`](#get_bucket_tags)                       | [`is_object_legal_hold_enabled`](#is_object_legal_hold_enabled) |                                                   |
-| [`set_bucket_tags`](#set_bucket_tags)                       |                                                                 |                                                   |
-| [`delete_bucket_policy`](#delete_bucket_policy)             |                                                                 |                                                   |
+| [`set_bucket_tags`](#set_bucket_tags)                       | [`get_object_retention`](#get_object_retention)                 |                                                   |
+| [`delete_bucket_policy`](#delete_bucket_policy)             | [`set_object_retention`](#set_object_retention)                 |                                                   |
 | [`get_bucket_policy`](#get_bucket_policy)                   |                                                                 |                                                   |
 | [`set_bucket_policy`](#set_bucket_policy)                   |                                                                 |                                                   |
 | [`delete_bucket_notification`](#delete_bucket_notification) |                                                                 |                                                   |
@@ -57,7 +57,7 @@ s3Client = Minio(
 | [`listen_bucket_notification`](#listen_bucket_notification) |                                                                 |                                                   |
 | [`delete_bucket_encryption`](#delete_bucket_encryption)     |                                                                 |                                                   |
 | [`get_bucket_encryption`](#get_bucket_encryption)           |                                                                 |                                                   |
-| [`put_bucket_encryption`](#put_bucket_encryption)           |                                                                 |                                                   |
+| [`set_bucket_encryption`](#set_bucket_encryption)           |                                                                 |                                                   |
 | [`delete_object_lock_config`](#delete_object_lock_config)   |                                                                 |                                                   |
 | [`get_object_lock_config`](#get_object_lock_config)         |                                                                 |                                                   |
 | [`set_object_lock_config`](#set_object_lock_config)         |                                                                 |                                                   |
@@ -453,9 +453,9 @@ __Parameters__
 
 __Return Value__
 
-| Param                               |
-|:------------------------------------|
-| Encryption configuration as _dict_. |
+| Param               |
+|:--------------------|
+| _SSEConfig_ object. |
 
 __Example__
 
@@ -463,32 +463,25 @@ __Example__
 config = minio.get_bucket_encryption("my-bucketname")
 ```
 
-<a name="put_bucket_encryption"></a>
+<a name="set_bucket_encryption"></a>
 
-### put_bucket_encryption(bucket_name, encryption_configuration)
+### set_bucket_encryption(bucket_name, config)
 
 Set encryption configuration of a bucket.
 
 __Parameters__
 
-| Param           | Type   | Description                                       |
-|:----------------|:-------|:--------------------------------------------------|
-| ``bucket_name`` | _str_  | Name of the bucket.                               |
-| ``enc_config``  | _dict_ | Encryption configuration as dictionary to be set. |
+| Param           | Type        | Description                           |
+|:----------------|:------------|:--------------------------------------|
+| ``bucket_name`` | _str_       | Name of the bucket.                   |
+| ``config``      | _SSEConfig_ | Server-side encryption configuration. |
 
 __Example__
 
 ```py
-# Sample default encryption configuration
-config = {
-    'ServerSideEncryptionConfiguration':{
-        'Rule': [
-            {'ApplyServerSideEncryptionByDefault': {'SSEAlgorithm': 'AES256'}}
-        ]
-    }
-}
-
-minio.put_bucket_encryption("my-bucketname", config)
+minio.set_bucket_encryption(
+    "my-bucketname", SSEConfig(Rule.new_sse_s3_rule()),
+)
 ```
 
 <a name="delete_bucket_encryption"></a>
@@ -1296,6 +1289,55 @@ __Example__
 minio.is_object_legal_hold_enabled("my-bucketname", "my-objectname")
 ```
 
+<a name="get_object_retention"></a>
+
+### get_object_retention(bucket_name, object_name, version_id=None)
+
+Get retention information of an object.
+
+__Parameters__
+
+| Param                | Type             | Description                                |
+|:---------------------|:-----------------|:-------------------------------------------|
+| `bucket_name`        | _str_            | Name of the bucket.                        |
+| `object_name`        | _str_            | Object name in the bucket.                 |
+| `version_id`         | _str_            | Version ID of the object.                  |
+
+__Return Value__
+
+| Return             |
+|:-------------------|
+| _Retention_ object |
+
+
+__Example__
+
+```py
+config = minio.get_object_retention("my-bucketname", "my-objectname")
+```
+
+<a name="set_object_retention"></a>
+
+### set_object_retention(bucket_name, object_name, config, version_id=None)
+
+Set retention information to an object.
+
+__Parameters__
+
+| Param         | Type        | Description                |
+|:--------------|:------------|:---------------------------|
+| `bucket_name` | _str_       | Name of the bucket.        |
+| `object_name` | _str_       | Object name in the bucket. |
+| `config`      | _Retention_ | Retention configuration.   |
+| `version_id`  | _str_       | Version ID of the object.  |
+
+__Example__
+
+```py
+config = Retention(GOVERNANCE, datetime.utcnow() + timedelta(days=10))
+minio.set_object_retention("my-bucketname", "my-objectname", config)
+```
+
 ## 4. Presigned operations
 
 <a name="presigned_get_object"></a>
diff --git a/examples/remove_bucket_encryption.py b/examples/delete_bucket_encryption.py
similarity index 68%
rename from examples/remove_bucket_encryption.py
rename to examples/delete_bucket_encryption.py
index b618602..97429b8 100644
--- a/examples/remove_bucket_encryption.py
+++ b/examples/delete_bucket_encryption.py
@@ -18,15 +18,11 @@
 # dummy values, please replace them with original values.
 
 from minio import Minio
-from minio.error import ResponseError
 
-client = Minio('s3.amazonaws.com',
-               access_key='YOUR-ACCESSKEYID',
-               secret_key='YOUR-SECRETACCESSKEY',
-               secure=True)
+client = Minio(
+    "play.min.io",
+    access_key="Q3AM3UQ867SPQQA43P2F",
+    secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
+)
 
-try:
-    # Delete default encryption configuration on bucket 'my-bucketname'.
-    client.delete_bucket_encryption('my-bucketname')
-except ResponseError as err:
-    print(err)
+client.delete_bucket_encryption("my-bucketname")
diff --git a/examples/get_bucket_encryption.py b/examples/get_bucket_encryption.py
index 78377a5..ff502c3 100644
--- a/examples/get_bucket_encryption.py
+++ b/examples/get_bucket_encryption.py
@@ -18,15 +18,11 @@
 # dummy values, please replace them with original values.
 
 from minio import Minio
-from minio.error import ResponseError
 
-client = Minio('s3.amazonaws.com',
-               access_key='YOUR-ACCESSKEYID',
-               secret_key='YOUR-SECRETACCESSKEY',
-               secure=True)
+client = Minio(
+    "play.min.io",
+    access_key="Q3AM3UQ867SPQQA43P2F",
+    secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
+)
 
-try:
-    # Get current policy of bucket 'my-bucketname'.
-    print(client.get_bucket_encryption('my-bucketname'))
-except ResponseError as err:
-    print(err)
+config = client.get_bucket_encryption("my-bucketname")
diff --git a/examples/get_object_retention.py b/examples/get_object_retention.py
new file mode 100644
index 0000000..9bb9bde
--- /dev/null
+++ b/examples/get_object_retention.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# MinIO Python Library for Amazon S3 Compatible Cloud Storage.
+# Copyright (C) 2020 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.
+
+# Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY and my-bucketname are
+# dummy values, please replace them with original values.
+
+from minio import Minio
+
+client = Minio(
+    "play.min.io",
+    access_key="Q3AM3UQ867SPQQA43P2F",
+    secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
+)
+
+config = client.get_object_retention("my-bucketname", "my-objectname")
diff --git a/examples/set_bucket_encryption.py b/examples/set_bucket_encryption.py
index daad2a9..58eb1be 100644
--- a/examples/set_bucket_encryption.py
+++ b/examples/set_bucket_encryption.py
@@ -18,27 +18,14 @@
 # dummy values, please replace them with original values.
 
 from minio import Minio
-from minio.error import ResponseError
+from minio.sseconfig import Rule, SSEConfig
 
-client = Minio('s3.amazonaws.com',
-               access_key='YOUR-ACCESSKEYID',
-               secret_key='YOUR-SECRETACCESSKEY',
-               secure=True)
+client = Minio(
+    "play.min.io",
+    access_key="Q3AM3UQ867SPQQA43P2F",
+    secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
+)
 
-try:
-    # Set default encryption configuration for bucket 'my-bucketname'
-    ENC_CONFIG = {
-        'ServerSideEncryptionConfiguration': {
-            'Rule': [
-                {
-                    'ApplyServerSideEncryptionByDefault': {
-                        'SSEAlgorithm': 'AES256'
-                    }
-                }
-            ]
-        }
-    }
-
-    client.put_bucket_encryption('my-bucketname', ENC_CONFIG)
-except ResponseError as err:
-    print(err)
+client.set_bucket_encryption(
+    "my-bucketname", SSEConfig(Rule.new_sse_s3_rule()),
+)
diff --git a/examples/set_object_retention.py b/examples/set_object_retention.py
new file mode 100644
index 0000000..b79617a
--- /dev/null
+++ b/examples/set_object_retention.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# MinIO Python Library for Amazon S3 Compatible Cloud Storage.
+# Copyright (C) 2020 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.
+
+# Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY and my-bucketname are
+# dummy values, please replace them with original values.
+
+from datetime import datetime, timedelta
+
+from minio import Minio
+from minio.commonconfig import GOVERNANCE
+from minio.retention import Retention
+
+client = Minio(
+    "play.min.io",
+    access_key="Q3AM3UQ867SPQQA43P2F",
+    secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
+)
+
+config = Retention(GOVERNANCE, datetime.utcnow() + timedelta(days=10))
+client.set_object_retention("my-bucketname", "my-objectname", config)
diff --git a/minio/api.py b/minio/api.py
index bf8692c..ec8c8ed 100644
--- a/minio/api.py
+++ b/minio/api.py
@@ -45,7 +45,8 @@ import urllib3
 from . import __title__, __version__
 from .commonconfig import Tags
 from .credentials import StaticProvider
-from .datatypes import ListAllMyBucketsResult, Object, parse_list_objects
+from .datatypes import (CompleteMultipartUploadResult, ListAllMyBucketsResult,
+                        Object, parse_list_objects)
 from .definitions import BaseURL, ObjectWriteResult, Part
 from .deleteobjects import DeleteError, DeleteRequest, DeleteResult
 from .error import InvalidResponseError, S3Error, ServerError
@@ -62,19 +63,19 @@ from .objectlockconfig import ObjectLockConfig
 from .parsers import (parse_error_response,
                       parse_list_multipart_uploads,
                       parse_list_parts,
-                      parse_multipart_upload_result,
                       parse_new_multipart_upload)
 from .replicationconfig import ReplicationConfig
+from .retention import Retention
 from .select import SelectObjectReader
 from .selectrequest import SelectRequest
 from .signer import (AMZ_DATE_FORMAT, SIGN_V4_ALGORITHM, get_credential_string,
                      post_presign_v4, presign_v4, sign_v4_s3)
 from .sse import SseCustomerKey
+from .sseconfig import SSEConfig
 from .tagging import Tagging
 from .thread_pool import ThreadPool
 from .versioningconfig import VersioningConfig
 from .xml import Element, SubElement, findtext, getbytes, marshal, unmarshal
-from .xml_marshal import xml_marshal_bucket_encryption, xml_to_dict
 
 try:
     from json.decoder import JSONDecodeError
@@ -772,22 +773,22 @@ class Minio:  # pylint: disable=too-many-public-methods
         """
         self.set_bucket_notification(bucket_name, NotificationConfig())
 
-    def put_bucket_encryption(self, bucket_name, enc_config):
+    def set_bucket_encryption(self, bucket_name, config):
         """
         Set encryption configuration of a bucket.
 
         :param bucket_name: Name of the bucket.
-        :param enc_config: Encryption configuration as dictionary to be set.
+        :param config: :class:`SSEConfig <SSEConfig>` object.
 
         Example::
-            minio.put_bucket_encryption("my-bucketname", config)
+            minio.set_bucket_encryption(
+                "my-bucketname", SSEConfig(Rule.new_sse_s3_rule()),
+            )
         """
         check_bucket_name(bucket_name)
-
-        # 'Rule' is a list, so we need to go through each one of
-        # its key/value pair and collect the encryption values.
-        rules = enc_config['ServerSideEncryptionConfiguration']['Rule']
-        body = xml_marshal_bucket_encryption(rules)
+        if not isinstance(config, SSEConfig):
+            raise ValueError("config must be SSEConfig type")
+        body = marshal(config)
         self._execute(
             "PUT",
             bucket_name,
@@ -801,18 +802,23 @@ class Minio:  # pylint: disable=too-many-public-methods
         Get encryption configuration of a bucket.
 
         :param bucket_name: Name of the bucket.
-        :return: Encryption configuration.
+        :return: :class:`SSEConfig <SSEConfig>` object.
 
         Example::
             config = minio.get_bucket_encryption("my-bucketname")
         """
         check_bucket_name(bucket_name)
-        response = self._execute(
-            "GET",
-            bucket_name,
-            query_params={"encryption": ""},
-        )
-        return xml_to_dict(response.data.decode())
+        try:
+            response = self._execute(
+                "GET",
+                bucket_name,
+                query_params={"encryption": ""},
+            )
+            return unmarshal(SSEConfig, response.data.decode())
+        except S3Error as exc:
+            if exc.code != "ServerSideEncryptionConfigurationNotFoundError":
+                raise
+        return None
 
     def delete_bucket_encryption(self, bucket_name):
         """
@@ -824,11 +830,15 @@ class Minio:  # pylint: disable=too-many-public-methods
             minio.delete_bucket_encryption("my-bucketname")
         """
         check_bucket_name(bucket_name)
-        self._execute(
-            "DELETE",
-            bucket_name,
-            query_params={"encryption": ""},
-        )
+        try:
+            self._execute(
+                "DELETE",
+                bucket_name,
+                query_params={"encryption": ""},
+            )
+        except S3Error as exc:
+            if exc.code != "ServerSideEncryptionConfigurationNotFoundError":
+                raise
 
     def listen_bucket_notification(self, bucket_name, prefix='', suffix='',
                                    events=('s3:ObjectCreated:*',
@@ -1190,10 +1200,7 @@ class Minio:  # pylint: disable=too-many-public-methods
             },
             query_params={'uploadId': upload_id},
         )
-        return (
-            parse_multipart_upload_result(response.data),
-            response.getheader("x-amz-version-id"),
-        )
+        return CompleteMultipartUploadResult(response)
 
     def _create_multipart_upload(self, bucket_name, object_name, headers):
         """Execute CreateMultipartUpload S3 API."""
@@ -1359,7 +1366,7 @@ class Minio:  # pylint: disable=too-many-public-methods
             result = self._complete_multipart_upload(
                 bucket_name, object_name, upload_id, parts,
             )
-            return result[0].etag, result[1]
+            return result.etag, result.version_id
         except Exception as exc:
             if upload_id:
                 self._abort_multipart_upload(
@@ -2235,6 +2242,74 @@ class Minio:  # pylint: disable=too-many-public-methods
             query_params={"object-lock": ""},
         )
 
+    def get_object_retention(
+            self, bucket_name, object_name, version_id=None,
+    ):
+        """
+        Get retention configuration of an object.
+
+        :param bucket_name: Name of the bucket.
+        :param object_name: Object name in the bucket.
+        :param version_id: Version ID of the object.
+        :return: :class:`Retention <Retention>` object.
+
+        Example::
+            config = minio.get_object_retention(
+                "my-bucketname", "my-objectname",
+            )
+        """
+        check_bucket_name(bucket_name)
+        check_non_empty_string(object_name)
+        query_params = {"versionId", version_id} if version_id else {}
+        query_params["retention"] = ""
+        try:
+            response = self._execute(
+                "GET",
+                bucket_name,
+                object_name=object_name,
+                query_params=query_params,
+            )
+            return unmarshal(Retention, response.data.decode())
+        except S3Error as exc:
+            if exc.code != "NoSuchObjectLockConfiguration":
+                raise
+        return None
+
+    def set_object_retention(
+            self, bucket_name, object_name, config, version_id=None,
+    ):
+        """
+        Set retention configuration on an object.
+
+        :param bucket_name: Name of the bucket.
+        :param object_name: Object name in the bucket.
+        :param version_id: Version ID of the object.
+        :param config: :class:`Retention <Retention>` object.
+
+        Example::
+            config = Retention(
+                GOVERNANCE, datetime.utcnow() + timedelta(days=10),
+            )
+            minio.set_object_retention(
+                "my-bucketname", "my-objectname", config,
+            )
+        """
+        check_bucket_name(bucket_name)
+        check_non_empty_string(object_name)
+        if not isinstance(config, Retention):
+            raise ValueError("config must be Retention type")
+        body = marshal(config)
+        query_params = {"versionId", version_id} if version_id else {}
+        query_params["retention"] = ""
+        self._execute(
+            "PUT",
+            bucket_name,
+            object_name=object_name,
+            body=body,
+            headers={"Content-MD5": md5sum_hash(body)},
+            query_params=query_params,
+        )
+
     def _list_objects(  # pylint: disable=too-many-arguments,too-many-branches
             self,
             bucket_name,
diff --git a/minio/datatypes.py b/minio/datatypes.py
index 3b5d7ed..d5abb60 100644
--- a/minio/datatypes.py
+++ b/minio/datatypes.py
@@ -233,3 +233,48 @@ def parse_list_objects(response, bucket_name):
     if continuation_token is None and is_truncated:
         continuation_token = marker
     return objects, is_truncated, continuation_token, version_id_marker
+
+
+class CompleteMultipartUploadResult:
+    """CompleteMultipartUpload API result."""
+
+    def __init__(self, response):
+        element = ET.fromstring(response.data.decode())
+        self._bucket_name = findtext(element, "Bucket")
+        self._object_name = findtext(element, "Key")
+        self._location = findtext(element, "Location")
+        self._etag = findtext(element, "ETag")
+        if self._etag:
+            self._etag = self._etag.replace('"', "")
+        self._version_id = response.getheader("x-amz-version-id")
+        self._http_headers = response.getheaders()
+
+    @property
+    def bucket_name(self):
+        """Get bucket name."""
+        return self._bucket_name
+
+    @property
+    def object_name(self):
+        """Get object name."""
+        return self._object_name
+
+    @property
+    def location(self):
+        """Get location."""
+        return self._location
+
+    @property
+    def etag(self):
+        """Get etag."""
+        return self._etag
+
+    @property
+    def version_id(self):
+        """Get version ID."""
+        return self._version_id
+
+    @property
+    def http_headers(self):
+        """Get HTTP headers."""
+        return self._http_headers
diff --git a/minio/definitions.py b/minio/definitions.py
index 06753a4..ace25ff 100644
--- a/minio/definitions.py
+++ b/minio/definitions.py
@@ -257,30 +257,6 @@ class BaseURL:
         return url
 
 
-class MultipartUploadResult:
-    """
-    A completed multipart upload metadata
-         :class:`MultipartUploadResult <MultipartUploadResult>`.
-
-    :param bucket_name: Bucket name.
-    :param object_name: Object name.
-    :param location: Object uploaded location.
-    :param etag: Object final etag.
-    """
-
-    def __init__(self, bucket_name, object_name, location, etag):
-        self.bucket_name = bucket_name
-        self.object_name = object_name
-        self.location = location
-        self.etag = etag
-
-    def __str__(self):
-        string_format = ("<IncompleteUpload: bucket_name: {0}"
-                         " object_name: {1} location: {2} etag: {3}>")
-        return string_format.format(self.bucket_name, self.object_name,
-                                    self.location, self.etag)
-
-
 class Upload:
     """ Upload information of a multipart upload."""
 
diff --git a/minio/parsers.py b/minio/parsers.py
index 6fb40ef..d2f7898 100644
--- a/minio/parsers.py
+++ b/minio/parsers.py
@@ -30,8 +30,7 @@ from urllib.parse import unquote
 from xml.etree import ElementTree
 from xml.etree.ElementTree import ParseError
 
-from .definitions import (ListMultipartUploadsResult, ListPartsResult,
-                          MultipartUploadResult)
+from .definitions import ListMultipartUploadsResult, ListPartsResult
 from .error import S3Error
 from .helpers import strptime_rfc3339
 
@@ -168,23 +167,6 @@ def parse_error_response(response):
     )
 
 
-def parse_multipart_upload_result(data):
-    """
-    Parser for complete multipart upload response.
-
-    :param data: Response data for complete multipart upload.
-    :return: :class:`MultipartUploadResult <MultipartUploadResult>`.
-    """
-    root = S3Element.fromstring('CompleteMultipartUploadResult', data)
-
-    return MultipartUploadResult(
-        root.get_child_text('Bucket'),
-        root.get_child_text('Key'),
-        root.get_child_text('Location'),
-        root.get_etag_elem()
-    )
-
-
 def parse_new_multipart_upload(data):
     """
     Parser for new multipart upload response.
diff --git a/minio/retention.py b/minio/retention.py
new file mode 100644
index 0000000..97d07d2
--- /dev/null
+++ b/minio/retention.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+# MinIO Python Library for Amazon S3 Compatible Cloud Storage, (C)
+# 2020 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.
+
+"""Request/response of PutObjectRetention and GetObjectRetention APIs."""
+
+from __future__ import absolute_import
+
+import datetime
+
+from .commonconfig import COMPLIANCE, GOVERNANCE
+from .helpers import strftime_rfc3339, strptime_rfc3339
+from .xml import Element, SubElement, findtext
+
+
+class Retention:
+    """Retention configuration."""
+
+    def __init__(self, mode, retain_until_date):
+        if mode not in [GOVERNANCE, COMPLIANCE]:
+            raise ValueError(
+                "mode must be {0} or {1}".format(GOVERNANCE, COMPLIANCE),
+            )
+        if not isinstance(retain_until_date, datetime.datetime):
+            raise ValueError(
+                "retain until date must be datetime.datetime type",
+            )
+        self._mode = mode
+        self._retain_until_date = retain_until_date
+
+    @property
+    def mode(self):
+        """Get mode."""
+        return self._mode
+
+    @property
+    def retain_until_date(self):
+        """Get retain util date."""
+        return self._retain_until_date
+
+    @classmethod
+    def fromxml(cls, element):
+        """Create new object with values from XML element."""
+        mode = findtext(element, "Mode", True)
+        retain_until_date = strptime_rfc3339(
+            findtext(element, "RetainUntilDate", True),
+        )
+        return cls(mode, retain_until_date)
+
+    def toxml(self, element):
+        """Convert to XML."""
+        element = Element("Retention")
+        SubElement(element, "Mode", self._mode)
+        SubElement(
+            element,
+            "RetainUntilDate",
+            strftime_rfc3339(self._retain_until_date),
+        )
+        return element
diff --git a/minio/sseconfig.py b/minio/sseconfig.py
new file mode 100644
index 0000000..b06fffd
--- /dev/null
+++ b/minio/sseconfig.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+# MinIO Python Library for Amazon S3 Compatible Cloud Storage, (C)
+# 2020 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.
+
+"""Request/response of PutBucketEncryption and GetBucketEncryption APIs."""
+
+from __future__ import absolute_import
+
+from abc import ABCMeta
+
+from .xml import Element, SubElement, find, findtext
+
+AES256 = "AES256"
+AWS_KMS = "aws:kms"
+
+
+class Rule:
+    """Server-side encryption rule. """
+    __metaclass__ = ABCMeta
+
+    def __init__(self, sse_algorithm, kms_master_key_id=None):
+        self._sse_algorithm = sse_algorithm
+        self._kms_master_key_id = kms_master_key_id
+
+    @property
+    def sse_algorithm(self):
+        """Get SSE algorithm."""
+        return self._sse_algorithm
+
+    @property
+    def kms_master_key_id(self):
+        """Get KMS master key ID."""
+        return self._kms_master_key_id
+
+    @classmethod
+    def new_sse_s3_rule(cls):
+        """Create SSE-S3 rule."""
+        return cls(AES256)
+
+    @classmethod
+    def new_sse_kms_rule(cls, kms_master_key_id=None):
+        """Create new SSE-KMS rule."""
+        return cls(AWS_KMS, kms_master_key_id)
+
+    @classmethod
+    def fromxml(cls, element):
+        """Create new object with values from XML element."""
+        element = find(element, "ApplyServerSideEncryptionByDefault")
+        sse_algorithm = findtext(element, "SSEAlgorithm", True)
+        kms_master_key_id = findtext(element, "KMSMasterKeyID")
+        return cls(sse_algorithm, kms_master_key_id)
+
+    def toxml(self, element):
+        """Convert to XML."""
+        element = SubElement(element, "Rule")
+        tag = SubElement(element, "ApplyServerSideEncryptionByDefault")
+        SubElement(tag, "SSEAlgorithm", self._sse_algorithm)
+        if self._kms_master_key_id is not None:
+            SubElement(tag, "KMSMasterKeyID", self._kms_master_key_id)
+        return element
+
+
+class SSEConfig:
+    """server-side encyption configuration."""
+
+    def __init__(self, rule):
+        if not rule:
+            raise ValueError("rule must be provided")
+        self._rule = rule
+
+    @property
+    def rule(self):
+        """Get rule."""
+        return self._rule
+
+    @classmethod
+    def fromxml(cls, element):
+        """Create new object with values from XML element."""
+        element = find(element, "Rule")
+        return cls(Rule.fromxml(element))
+
+    def toxml(self, element):
+        """Convert to XML."""
+        element = Element("ServerSideEncryptionConfiguration")
+        self._rule.toxml(element)
+        return element
diff --git a/minio/xml_marshal.py b/minio/xml_marshal.py
deleted file mode 100644
index 2b1dfd3..0000000
--- a/minio/xml_marshal.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# -*- coding: utf-8 -*-
-# MinIO Python Library for Amazon S3 Compatible Cloud Storage, (C)
-# 2015, 2016, 2017, 2018, 2019 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.
-
-"""
-minio.xml_marshal
-~~~~~~~~~~~~~~~
-
-This module contains the simple wrappers for XML marshaller's.
-
-:copyright: (c) 2015 by MinIO, Inc.
-:license: Apache 2.0, see LICENSE for more details.
-
-"""
-
-from __future__ import absolute_import
-
-import io
-from collections import defaultdict
-from xml.etree import ElementTree as ET
-
-_S3_NAMESPACE = 'http://s3.amazonaws.com/doc/2006-03-01/'
-
-
-def Element(tag, with_namespace=False):  # pylint: disable=invalid-name
-    """Create ElementTree.Element with tag and namespace."""
-    if with_namespace:
-        return ET.Element(tag, {'xmlns': _S3_NAMESPACE})
-    return ET.Element(tag)
-
-
-def SubElement(parent, tag, text=None):  # pylint: disable=invalid-name
-    """Create ElementTree.SubElement on parent with tag and text."""
-    element = ET.SubElement(parent, tag)
-    if text is not None:
-        element.text = text
-    return element
-
-
-def _get_xml_data(element):
-    """Get XML data of ElementTree.Element."""
-    data = io.BytesIO()
-    ET.ElementTree(element).write(data, encoding=None, xml_declaration=False)
-    return data.getvalue()
-
-
-def _etree_to_dict(elem):
-    """Converts ElementTree object to dict."""
-    ns = '{' + _S3_NAMESPACE + '}'  # pylint: disable=invalid-name
-    elem.tag = elem.tag.replace(ns, '')
-
-    d = {elem.tag: {} if elem.attrib else None}  # pylint: disable=invalid-name
-    children = list(elem)
-    if children:
-        dd = defaultdict(list)  # pylint: disable=invalid-name
-        is_rule = children[0].tag.replace(ns, "") == "Rule"
-        # pylint: disable=invalid-name
-        for dc in map(_etree_to_dict, children):
-            for k, v in dc.items():  # pylint: disable=invalid-name
-                dd[k].append([v] if is_rule else v)
-        # pylint: disable=invalid-name
-        d = {elem.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}}
-    if elem.attrib:
-        d[elem.tag].update(('@' + k, v) for k, v in elem.attrib.items())
-    if elem.text:
-        text = elem.text.strip()
-        if children or elem.attrib:
-            if text:
-                d[elem.tag]['#text'] = text
-        else:
-            d[elem.tag] = text
-    return d
-
-
-def xml_to_dict(in_xml):
-    """Convert XML to dict."""
-    elem = ET.XML(in_xml)
-    return _etree_to_dict(elem)
-
-
-def xml_marshal_bucket_encryption(rules):
-    """Encode bucket encryption to XML."""
-
-    root = Element('ServerSideEncryptionConfiguration')
-
-    if rules:
-        # As server supports only one rule, the first rule is taken due to
-        # no validation is done at server side.
-        apply_element = SubElement(SubElement(root, 'Rule'),
-                                   'ApplyServerSideEncryptionByDefault')
-        SubElement(apply_element, 'SSEAlgorithm',
-                   rules[0]['ApplyServerSideEncryptionByDefault'].get(
-                       'SSEAlgorithm', 'AES256'))
-        kms_text = rules[0]['ApplyServerSideEncryptionByDefault'].get(
-            'KMSMasterKeyID')
-        if kms_text:
-            SubElement(apply_element, 'KMSMasterKeyID', kms_text)
-
-    return _get_xml_data(root)


Test patch:
diff --git a/tests/unit/retention_test.py b/tests/unit/retention_test.py
new file mode 100644
index 0000000..4bcd5dd
--- /dev/null
+++ b/tests/unit/retention_test.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# MinIO Python Library for Amazon S3 Compatible Cloud Storage,
+# (C) 2020 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.
+
+from datetime import datetime, timedelta
+from unittest import TestCase
+
+from nose.tools import eq_
+
+from minio import xml
+from minio.commonconfig import COMPLIANCE, GOVERNANCE
+from minio.retention import Retention
+
+
+class RetentionTest(TestCase):
+    def test_config(self):
+        config = Retention(GOVERNANCE, datetime.now() + timedelta(days=10))
+        xml.marshal(config)
+
+        config = xml.unmarshal(
+            Retention,
+            """<Retention xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
+   <Mode>COMPLIANCE</Mode>
+   <RetainUntilDate>2020-10-02T00:00:00Z</RetainUntilDate>
+</Retention>""",
+        )
+        xml.marshal(config)
+        eq_(config.mode, COMPLIANCE)
+        eq_(config.retain_until_date, datetime(2020, 10, 2, 0, 0, 0))
diff --git a/tests/unit/sseconfig_test.py b/tests/unit/sseconfig_test.py
new file mode 100644
index 0000000..512c821
--- /dev/null
+++ b/tests/unit/sseconfig_test.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# MinIO Python Library for Amazon S3 Compatible Cloud Storage,
+# (C) 2020 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.
+
+from unittest import TestCase
+
+from nose.tools import eq_
+
+from minio import xml
+from minio.sseconfig import AWS_KMS, Rule, SSEConfig
+
+
+class ReplicationConfigTest(TestCase):
+    def test_config(self):
+        config = SSEConfig(Rule.new_sse_s3_rule())
+        xml.marshal(config)
+
+        config = xml.unmarshal(
+            SSEConfig,
+            """<ServerSideEncryptionConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
+  <Rule>
+    <ApplyServerSideEncryptionByDefault>
+        <SSEAlgorithm>aws:kms</SSEAlgorithm>
+        <KMSMasterKeyID>arn:aws:kms:us-east-1:1234/5678example</KMSMasterKeyID>
+    </ApplyServerSideEncryptionByDefault>
+</Rule>
+</ServerSideEncryptionConfiguration>
+        """,
+        )
+        xml.marshal(config)
+        eq_(config.rule.sse_algorithm, AWS_KMS)
+        eq_(
+            config.rule.kms_master_key_id,
+            "arn:aws:kms:us-east-1:1234/5678example",
+        )