Repository: minio/minio-py
Pull request #982 (2020-09-30 14:20:17)

Add {set,get,delete}_{bucket,object}_tags APIs


Patch:
diff --git a/docs/API.md b/docs/API.md
index 5a6da59..e0a427f 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -41,10 +41,13 @@ s3Client = Minio(
 | [`set_bucket_versioning`](#set_bucket_versioning)         | [`fput_object`](#fput_object)                     |                                                   | [`listen_bucket_notification`](#listen_bucket_notification)         |
 | [`delete_bucket_replication`](#delete_bucket_replication) | [`fget_object`](#fget_object)                     |                                                   | [`get_bucket_encryption`](#get_bucket_encryption)                   |
 | [`get_bucket_replication`](#get_bucket_replication)       | [`select_object_content`](#select_object_content) |                                                   | [`remove_all_bucket_notification`](#remove_all_bucket_notification) |
-| [`set_bucket_replication`](#set_bucket_replication)       |                                                   |                                                   | [`put_bucket_encryption`](#put_bucket_encryption)                   |
-| [`delete_bucket_lifecycle`](#delete_bucket_lifecycle)     |                                                   |                                                   | [`delete_bucket_encryption`](#delete_bucket_encryption)             |
-| [`get_bucket_lifecycle`](#get_bucket_lifecycle)           |                                                   |                                                   |                                                                     |
+| [`set_bucket_replication`](#set_bucket_replication)       | [`delete_object_tags`](#delete_object_tags)       |                                                   | [`put_bucket_encryption`](#put_bucket_encryption)                   |
+| [`delete_bucket_lifecycle`](#delete_bucket_lifecycle)     | [`get_object_tags`](#get_object_tags)             |                                                   | [`delete_bucket_encryption`](#delete_bucket_encryption)             |
+| [`get_bucket_lifecycle`](#get_bucket_lifecycle)           | [`set_object_tags`](#set_object_tags)             |                                                   |                                                                     |
 | [`set_bucket_lifecycle`](#set_bucket_lifecycle)           |                                                   |                                                   |                                                                     |
+| [`delete_bucket_tags`](#delete_bucket_tags)               |                                                   |                                                   |                                                                     |
+| [`get_bucket_tags`](#get_bucket_tags)                     |                                                   |                                                   |                                                                     |
+| [`set_bucket_tags`](#set_bucket_tags)                     |                                                   |                                                   |                                                                     |
 
 ## 1. Constructor
 
@@ -776,6 +779,68 @@ config = LifecycleConfig(
 minio.set_bucket_lifecycle("my-bucketname", config)
 ```
 
+<a name="delete_bucket_tags"></a>
+
+### delete_bucket_tags(bucket_name)
+
+Delete tags configuration of a bucket.
+
+__Parameters__
+
+| Param           | Type  | Description         |
+|:----------------|:------|:--------------------|
+| ``bucket_name`` | _str_ | Name of the bucket. |
+
+__Example__
+
+```py
+minio.delete_bucket_tags("my-bucketname")
+```
+
+<a name="get_bucket_tags"></a>
+
+### get_bucket_tags(bucket_name)
+
+Get tags configuration of a bucket.
+
+__Parameters__
+
+| Param           | Type  | Description         |
+|:----------------|:------|:--------------------|
+| ``bucket_name`` | _str_ | Name of the bucket. |
+
+| Return         |
+|:---------------|
+| _Tags_ object. |
+
+__Example__
+
+```py
+tags = minio.get_bucket_tags("my-bucketname")
+```
+
+<a name="set_bucket_tags"></a>
+
+### set_bucket_tags(bucket_name, tags)
+
+Set tags configuration to a bucket.
+
+__Parameters__
+
+| Param           | Type   | Description         |
+|:----------------|:-------|:--------------------|
+| ``bucket_name`` | _str_  | Name of the bucket. |
+| ``tags``        | _Tags_ | Tags configuration. |
+
+__Example__
+
+```py
+tags = Tags.new_bucket_tags()
+tags["Project"] = "Project One"
+tags["User"] = "jsmith"
+client.set_bucket_tags("my-bucketname", tags)
+```
+
 ## 3. Object operations
 
 <a name="get_object"></a>
@@ -1112,6 +1177,74 @@ minio.remove_objects(
 )
 ```
 
+<a name="delete_object_tags"></a>
+
+### delete_object_tags(bucket_name, object_name, version_id=None)
+
+Delete tags configuration 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.  |
+
+__Example__
+
+```py
+minio.delete_object_tags("my-bucketname", "my-objectname")
+```
+
+<a name="get_object_tags"></a>
+
+### get_object_tags(bucket_name, object_name, version_id=None)
+
+Get tags configuration 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         |
+|:---------------|
+| _Tags_ object. |
+
+__Example__
+
+```py
+tags = minio.get_object_tags("my-bucketname", "my-objectname")
+```
+
+<a name="set_object_tags"></a>
+
+### set_object_tags(bucket_name, object_name, tags, version_id=None)
+
+Set tags configuration to an object.
+
+__Parameters__
+
+| Param           | Type   | Description                |
+|:----------------|:-------|:---------------------------|
+| ``bucket_name`` | _str_  | Name of the bucket.        |
+| ``object_name`` | _str_  | Object name in the bucket. |
+| ``tags``        | _Tags_ | Tags configuration.        |
+| ``version_id``  | _str_  | Version ID of the object.  |
+
+__Example__
+
+```py
+tags = Tags.new_object_tags()
+tags["Project"] = "Project One"
+tags["User"] = "jsmith"
+client.set_object_tags("my-bucketname", "my-objectname", tags)
+```
+
 ## 4. Presigned operations
 
 <a name="presigned_get_object"></a>
diff --git a/examples/delete_bucket_tags.py b/examples/delete_bucket_tags.py
new file mode 100644
index 0000000..f776726
--- /dev/null
+++ b/examples/delete_bucket_tags.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",
+)
+
+client.delete_bucket_tags("my-bucketname")
diff --git a/examples/delete_object_tags.py b/examples/delete_object_tags.py
new file mode 100644
index 0000000..79cf31e
--- /dev/null
+++ b/examples/delete_object_tags.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",
+)
+
+client.delete_object_tags("my-bucketname", "my-objectname")
diff --git a/examples/get_bucket_tags.py b/examples/get_bucket_tags.py
new file mode 100644
index 0000000..7c2b11b
--- /dev/null
+++ b/examples/get_bucket_tags.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",
+)
+
+tags = client.get_bucket_tags("my-bucketname")
diff --git a/examples/get_object_tags.py b/examples/get_object_tags.py
new file mode 100644
index 0000000..f1e40c7
--- /dev/null
+++ b/examples/get_object_tags.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",
+)
+
+tags = client.get_object_tags("my-bucketname", "my-objectname")
diff --git a/examples/set_bucket_tags.py b/examples/set_bucket_tags.py
new file mode 100644
index 0000000..9277fb8
--- /dev/null
+++ b/examples/set_bucket_tags.py
@@ -0,0 +1,32 @@
+# -*- 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
+from minio.commonconfig import Tags
+
+client = Minio(
+    "play.min.io",
+    access_key="Q3AM3UQ867SPQQA43P2F",
+    secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
+)
+
+tags = Tags.new_bucket_tags()
+tags["Project"] = "Project One"
+tags["User"] = "jsmith"
+client.set_bucket_tags("my-bucketname", tags)
diff --git a/examples/set_object_tags.py b/examples/set_object_tags.py
new file mode 100644
index 0000000..c7683e0
--- /dev/null
+++ b/examples/set_object_tags.py
@@ -0,0 +1,32 @@
+# -*- 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
+from minio.commonconfig import Tags
+
+client = Minio(
+    "play.min.io",
+    access_key="Q3AM3UQ867SPQQA43P2F",
+    secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
+)
+
+tags = Tags.new_object_tags()
+tags["Project"] = "Project One"
+tags["User"] = "jsmith"
+client.set_object_tags("my-bucketname", "my-objectname", tags)
diff --git a/minio/api.py b/minio/api.py
index c01cfbf..3531820 100644
--- a/minio/api.py
+++ b/minio/api.py
@@ -43,6 +43,7 @@ import dateutil.parser
 import urllib3
 
 from . import __title__, __version__
+from .commonconfig import Tags
 from .credentials import StaticProvider
 from .definitions import BaseURL, Object, ObjectWriteResult, Part
 from .error import InvalidResponseError, S3Error, ServerError
@@ -66,6 +67,7 @@ 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 .tagging import Tagging
 from .thread_pool import ThreadPool
 from .versioningconfig import VersioningConfig
 from .xml import Element, SubElement, findtext, marshal, unmarshal
@@ -1924,6 +1926,148 @@ class Minio:  # pylint: disable=too-many-public-methods
             query_params={"lifecycle": ""},
         )
 
+    def delete_bucket_tags(self, bucket_name):
+        """
+        Delete tags configuration of a bucket.
+
+        :param bucket_name: Name of the bucket.
+
+        Example::
+            minio.delete_bucket_tags("my-bucketname")
+        """
+        check_bucket_name(bucket_name)
+        self._execute("DELETE", bucket_name, query_params={"tagging": ""})
+
+    def get_bucket_tags(self, bucket_name):
+        """
+        Get tags configuration of a bucket.
+
+        :param bucket_name: Name of the bucket.
+        :return: :class:`Tags <Tags>` object.
+
+        Example::
+            tags = minio.get_bucket_tags("my-bucketname")
+        """
+        check_bucket_name(bucket_name)
+        try:
+            response = self._execute(
+                "GET", bucket_name, query_params={"tagging": ""},
+            )
+            tagging = unmarshal(Tagging, response.data.decode())
+            return tagging.tags()
+        except S3Error as exc:
+            if exc.code != "NoSuchTagSet":
+                raise
+        return None
+
+    def set_bucket_tags(self, bucket_name, tags):
+        """
+        Set tags configuration to a bucket.
+
+        :param bucket_name: Name of the bucket.
+        :param tags: :class:`Tags <Tags>` object.
+
+        Example::
+            tags = Tags.new_bucket_tags()
+            tags["Project"] = "Project One"
+            tags["User"] = "jsmith"
+            minio.set_bucket_tags("my-bucketname", tags)
+        """
+        check_bucket_name(bucket_name)
+        if not isinstance(tags, Tags):
+            raise ValueError("tags must be Tags type")
+        body = marshal(Tagging(tags))
+        self._execute(
+            "PUT",
+            bucket_name,
+            body=body,
+            headers={"Content-MD5": md5sum_hash(body)},
+            query_params={"tagging": ""},
+        )
+
+    def delete_object_tags(self, bucket_name, object_name, version_id=None):
+        """
+        Delete tags 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.
+
+        Example::
+            minio.delete_object_tags("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["tagging"] = ""
+        self._execute(
+            "DELETE",
+            bucket_name,
+            object_name=object_name,
+            query_params=query_params,
+        )
+
+    def get_object_tags(self, bucket_name, object_name, version_id=None):
+        """
+        Get tags configuration of a 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:`Tags <Tags>` object.
+
+        Example::
+            tags = minio.get_object_tags("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["tagging"] = ""
+        try:
+            response = self._execute(
+                "GET",
+                bucket_name,
+                object_name=object_name,
+                query_params=query_params,
+            )
+            tagging = unmarshal(Tagging, response.data.decode())
+            return tagging.tags()
+        except S3Error as exc:
+            if exc.code != "NoSuchTagSet":
+                raise
+        return None
+
+    def set_object_tags(self, bucket_name, object_name, tags, version_id=None):
+        """
+        Set tags configuration to 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 tags: :class:`Tags <Tags>` object.
+
+        Example::
+            tags = Tags.new_object_tags()
+            tags["Project"] = "Project One"
+            tags["User"] = "jsmith"
+            minio.set_object_tags("my-bucketname", "my-objectname", tags)
+        """
+        check_bucket_name(bucket_name)
+        check_non_empty_string(object_name)
+        if not isinstance(tags, Tags):
+            raise ValueError("tags must be Tags type")
+        body = marshal(Tagging(tags))
+        query_params = {"versionId": version_id} if version_id else {}
+        query_params["tagging"] = ""
+        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/commonconfig.py b/minio/commonconfig.py
index c591798..85bf6d7 100644
--- a/minio/commonconfig.py
+++ b/minio/commonconfig.py
@@ -52,6 +52,16 @@ class Tags(dict):
             raise ValueError("invalid tag value '{0}'".format(value))
         super().__setitem__(key, value)
 
+    @classmethod
+    def new_bucket_tags(cls):
+        """Create new bucket tags."""
+        return cls()
+
+    @classmethod
+    def new_object_tags(cls):
+        """Create new object tags."""
+        return cls(True)
+
     @classmethod
     def fromxml(cls, element):
         """Create new object with values from XML element."""
diff --git a/minio/tagging.py b/minio/tagging.py
new file mode 100644
index 0000000..ac186ab
--- /dev/null
+++ b/minio/tagging.py
@@ -0,0 +1,52 @@
+# -*- 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.
+
+"""Tagging for bucket and object."""
+
+from __future__ import absolute_import
+
+from .commonconfig import Tags
+from .xml import Element, SubElement, find
+
+
+class Tagging:
+    """Tagging for buckets and objects."""
+
+    def __init__(self, tags):
+        self._tags = tags
+
+    @property
+    def tags(self):
+        """Get tags."""
+        return self._tags
+
+    @classmethod
+    def fromxml(cls, element):
+        """Create new object with values from XML element."""
+        element = find(element, "TagSet")
+        tags = (
+            None if find(element, "Tag") is None
+            else Tags.fromxml(element)
+        )
+        return cls(tags)
+
+    def toxml(self, element):
+        """Convert to XML."""
+        element = Element("Tagging")
+        if self._tags:
+            element = SubElement(element, "TagSet")
+            self._tags.toxml(element)
+        return element


Test patch:
diff --git a/tests/unit/tagging.py b/tests/unit/tagging.py
new file mode 100644
index 0000000..b3ff772
--- /dev/null
+++ b/tests/unit/tagging.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 minio import xml
+from minio.commonconfig import Tags
+from minio.tagging import Tagging
+
+
+class TaggingTest(TestCase):
+    def test_tagging(self):
+        tags = Tags()
+        tags["Project"] = "Project One"
+        tags["User"] = "jsmith"
+        tagging = Tagging(tags)
+        xml.marshal(tagging)
+
+        config = xml.unmarshal(
+            Tagging,
+            """<Tagging xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
+  <TagSet>
+    <Tag>
+      <Key>key1</Key>
+      <Value>value1</Value>
+    </Tag>
+    <Tag>
+      <Key>key2</Key>
+      <Value>value2</Value>
+    </Tag>
+  </TagSet>
+</Tagging>""",
+        )
+        xml.marshal(config)