Repository: minio/minio-py
Pull request #986 (2020-10-02 13:51:22)

add {set,get}_object_retention() APIs

kannappanr: @balamurugana can this be reviewed?
balamurugana: > @balamurugana can this be reviewed?

Depends on #985

Patch:
diff --git a/docs/API.md b/docs/API.md
index 7ff06f1..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) |                                                                 |                                                   |
@@ -1289,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/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_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 89ebcea..56548b8 100644
--- a/minio/api.py
+++ b/minio/api.py
@@ -65,6 +65,7 @@ from .parsers import (parse_error_response,
                       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,
@@ -2244,6 +2245,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/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


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))