Skip to content

Commit

Permalink
Merge pull request #176 from coderall/master
Browse files Browse the repository at this point in the history
add tagging interface & tests
  • Loading branch information
coderall committed May 7, 2019
2 parents 4b1d079 + d54ced3 commit 6317035
Show file tree
Hide file tree
Showing 14 changed files with 1,028 additions and 10 deletions.
2 changes: 1 addition & 1 deletion oss2/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '2.6.1'
__version__ = '2.6.2'

from . import models, exceptions

Expand Down
45 changes: 45 additions & 0 deletions oss2/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ class Bucket(_Base):
STAT = 'stat'
BUCKET_INFO = 'bucketInfo'
PROCESS = 'x-oss-process'
TAGGING = 'tagging'

def __init__(self, auth, endpoint, bucket_name,
is_cname=False,
Expand Down Expand Up @@ -1595,6 +1596,50 @@ def process_object(self, key, process):
resp = self.__do_object('POST', key, params={Bucket.PROCESS: ''}, data=process_data)
logger.debug("Process object done, req_id: {0}, status_code: {1}".format(resp.request_id, resp.status))
return ProcessObjectResult(resp)

def put_object_tagging(self, key, tagging, headers=None):
"""
:param str key: 上传tagging的对象名称,不能为空。
:param tagging: tag 标签内容
:type tagging: :class:`ObjectTagging <oss2.models.ObjectTagging>` 对象
:return: :class:`RequestResult <oss2.models.RequestResult>`
"""
logger.debug("Start to put object tagging, bucket: {0}, key: {1}, tagging: {2}".format(
self.bucket_name, to_string(key), tagging))

if headers is not None:
headers = http.CaseInsensitiveDict(headers)

data = self.__convert_data(ObjectTagging, xml_utils.to_put_object_tagging, tagging)
resp = self.__do_object('PUT', key, data=data, params={Bucket.TAGGING: ''}, headers=headers)

return RequestResult(resp)

def get_object_tagging(self, key):

"""
:param str key: 要获取tagging的对象名称
:return: :class:`ObjectTagging <oss2.models.ObjectTagging>`
"""
logger.debug("Start to get object tagging, bucket: {0}, key: {1}".format(
self.bucket_name, to_string(key)))
resp = self.__do_object('GET', key, params={Bucket.TAGGING: ''})

return self._parse_result(resp, xml_utils.parse_get_object_tagging, GetObjectTaggingResult)

def delete_object_tagging(self, key):
"""
:param str key: 要删除tagging的对象名称
:return: :class:`RequestResult <oss2.models.RequestResult>`
"""
logger.debug("Start to delete object tagging, bucket: {0}, key: {1}".format(
self.bucket_name, to_string(key)))
resp = self.__do_object('DELETE', key, params={Bucket.TAGGING: ''})
logger.debug("Delete object tagging done, req_id: {0}, status_code: {1}".format(resp.request_id, resp.status))
return RequestResult(resp)

def _get_bucket_config(self, config):
"""获得Bucket某项配置,具体哪种配置由 `config` 指定。该接口直接返回 `RequestResult` 对象。
Expand Down
2 changes: 1 addition & 1 deletion oss2/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class Auth(AuthBase):
'response-expires', 'response-content-disposition', 'cors', 'lifecycle',
'restore', 'qos', 'referer', 'stat', 'bucketInfo', 'append', 'position', 'security-token',
'live', 'comp', 'status', 'vod', 'startTime', 'endTime', 'x-oss-process',
'symlink', 'callback', 'callback-var']
'symlink', 'callback', 'callback-var', 'tagging']
)

def _sign_request(self, req, bucket_name, key):
Expand Down
3 changes: 3 additions & 0 deletions oss2/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
OSS_SERVER_SIDE_ENCRYPTION = "x-oss-server-side-encryption"
OSS_SERVER_SIDE_ENCRYPTION_KEY_ID = "x-oss-server-side-encryption-key-id"

OSS_OBJECT_TAGGING = "x-oss-tagging"
OSS_OBJECT_TAGGING_COPY_DIRECTIVE = "x-oss-tagging-directive"


class RequestHeader(dict):
def __init__(self, *arg, **kw):
Expand Down
76 changes: 74 additions & 2 deletions oss2/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from .utils import http_to_unixtime, make_progress_adapter, make_crc_adapter
from .exceptions import ClientError, InconsistentError
from .compat import urlunquote, to_string
from .compat import urlunquote, to_string, urlquote
from .select_response import SelectResponseAdapter
from .headers import *
import json
Expand Down Expand Up @@ -552,6 +552,10 @@ class LifecycleRule(object):
:param expiration: 过期删除操作。
:type expiration: :class:`LifecycleExpiration`
:param status: 启用还是禁止该规则。可选值为 `LifecycleRule.ENABLED` 或 `LifecycleRule.DISABLED`
:param storage_transitions: 存储类型转换规则
:type storage_transitions: :class:`StorageTransition`
:param tagging: object tagging 规则
:type tagging: :class:`ObjectTagging`
"""

ENABLED = 'Enabled'
Expand All @@ -560,13 +564,14 @@ class LifecycleRule(object):
def __init__(self, id, prefix,
status=ENABLED, expiration=None,
abort_multipart_upload=None,
storage_transitions=None):
storage_transitions=None, tagging=None):
self.id = id
self.prefix = prefix
self.status = status
self.expiration = expiration
self.abort_multipart_upload = abort_multipart_upload
self.storage_transitions = storage_transitions
self.tagging = tagging


class BucketLifecycle(object):
Expand Down Expand Up @@ -878,3 +883,70 @@ def __init__(self, resp):
self.object = result['object']
if 'status' in result:
self.process_status = result['status']

_MAX_OBJECT_TAGGING_KEY_LENGTH=128
_MAX_OBJECT_TAGGING_VALUE_LENGTH=256

class ObjectTagging(object):

def __init__(self, tagging_rules=None):

self.tag_set = tagging_rules or ObjectTaggingRule()

def __str__(self):

tag_str = ""

tagging_rule = self.tag_set.tagging_rule

for key in tagging_rule:
tag_str += key
tag_str += ":" + tagging_rule[key] + " "

return tag_str

class ObjectTaggingRule(object):

def __init__(self):
self.tagging_rule = dict()

def add(self, key, value):

if key is None or key == '':
raise ClientError("ObjectTagging key should not be empty")

if len(key) > _MAX_OBJECT_TAGGING_KEY_LENGTH:
raise ClientError("ObjectTagging key is too long")

if len(value) > _MAX_OBJECT_TAGGING_VALUE_LENGTH:
raise ClientError("ObjectTagging value is too long")

self.tagging_rule[key] = value

def delete(self, key):
del self.tagging_rule[key]

def len(self):
return len(self.tagging_rule)

def to_query_string(self):
query_string = ''

for key in self.tagging_rule:
query_string += urlquote(key)
query_string += '='
query_string += urlquote(self.tagging_rule[key])
query_string += '&'

if len(query_string) == 0:
return ''
else:
query_string = query_string[:-1]

return query_string

class GetObjectTaggingResult(RequestResult, ObjectTagging):

def __init__(self, resp):
RequestResult.__init__(self, resp)
ObjectTagging.__init__(self)
57 changes: 55 additions & 2 deletions oss2/xml_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
Owner,
AccessControlList,
AbortMultipartUpload,
StorageTransition)
StorageTransition,
ObjectTagging,
ObjectTaggingRule)

from .compat import urlunquote, to_unicode, to_string
from .utils import iso8601_to_unixtime, date_to_iso8601, iso8601_to_date
Expand Down Expand Up @@ -422,21 +424,36 @@ def parse_lifecycle_storage_transitions(storage_transition_nodes):

return storage_transitions

def parse_lifecycle_object_taggings(lifecycle_tagging_nodes):

if lifecycle_tagging_nodes is None:
return ObjectTagging()

tagging_rule = ObjectTaggingRule()
for tag_node in lifecycle_tagging_nodes:
key = _find_tag(tag_node, 'Key')
value = _find_tag(tag_node, 'Value')
tagging_rule.add(key, value)

return ObjectTagging(tagging_rule)

def parse_get_bucket_lifecycle(result, body):
root = ElementTree.fromstring(body)
url_encoded = _is_url_encoding(root)

for rule_node in root.findall('Rule'):
expiration = parse_lifecycle_expiration(rule_node.find('Expiration'))
abort_multipart_upload = parse_lifecycle_abort_multipart_upload(rule_node.find('AbortMultipartUpload'))
storage_transitions = parse_lifecycle_storage_transitions(rule_node.findall('Transition'))
tagging = parse_lifecycle_object_taggings(rule_node.findall('Tag'))
rule = LifecycleRule(
_find_tag(rule_node, 'ID'),
_find_tag(rule_node, 'Prefix'),
status=_find_tag(rule_node, 'Status'),
expiration=expiration,
abort_multipart_upload=abort_multipart_upload,
storage_transitions=storage_transitions
storage_transitions=storage_transitions,
tagging=tagging
)
result.rules.append(rule)

Expand Down Expand Up @@ -567,6 +584,13 @@ def to_put_bucket_lifecycle(bucket_lifecycle):
_add_text_child(storage_transition_node, 'CreatedBeforeDate',
date_to_iso8601(storage_transition.created_before_date))

tagging = rule.tagging
if tagging:
tagging_rule = tagging.tag_set.tagging_rule
for key in tagging.tag_set.tagging_rule:
tag_node = ElementTree.SubElement(rule_node, 'Tag')
_add_text_child(tag_node, 'Key', key)
_add_text_child(tag_node, 'Value', tagging_rule[key])
return _node_to_string(root)


Expand Down Expand Up @@ -741,3 +765,32 @@ def to_get_select_json_object_meta(json_meta_param):
raise SelectOperationClientError("The json_meta_param contains unsupported key " + key, "")

return _node_to_string(root)

def to_put_object_tagging(object_tagging):
root = ElementTree.Element("Tagging")
tag_set = ElementTree.SubElement(root, "TagSet")

for item in object_tagging.tag_set.tagging_rule:
tag_xml = ElementTree.SubElement(tag_set, "Tag")
_add_text_child(tag_xml, 'Key', item)
_add_text_child(tag_xml, 'Value', object_tagging.tag_set.tagging_rule[item])

return _node_to_string(root)

def parse_get_object_tagging(result, body):
root = ElementTree.fromstring(body)
url_encoded = _is_url_encoding(root)
tagset_node = root.find('TagSet')

if tagset_node is None:
return result

tagging_rules = ObjectTaggingRule()
for tag_node in tagset_node.findall('Tag'):
key = _find_object(tag_node, 'Key', url_encoded)
value = _find_object(tag_node, 'Value', url_encoded)
tagging_rules.add(key, value)

result.tag_set = tagging_rules
return result

Empty file added tests/__init__.py
Empty file.
10 changes: 7 additions & 3 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
OSS_ID = os.getenv("OSS_TEST_ACCESS_KEY_ID")
OSS_SECRET = os.getenv("OSS_TEST_ACCESS_KEY_SECRET")
OSS_ENDPOINT = os.getenv("OSS_TEST_ENDPOINT")
OSS_BUCKET = os.getenv("OSS_TEST_BUCKET")
OSS_TEST_BUCKET = os.getenv("OSS_TEST_BUCKET")
OSS_CNAME = os.getenv("OSS_TEST_CNAME")
OSS_CMK = os.getenv("OSS_TEST_CMK")
OSS_REGION = os.getenv("OSS_TEST_REGION", "cn-hangzhou")
Expand All @@ -30,7 +30,11 @@
def random_string(n):
return ''.join(random.choice(string.ascii_lowercase) for i in range(n))


OSS_BUCKET = ''
if OSS_TEST_BUCKET is None:
OSS_BUCKET = 'aliyun-oss-python-sdk-'+random_string(10)
else:
OSS_BUCKET = OSS_TEST_BUCKET + random_string(10)

def random_bytes(n):
return oss2.to_bytes(random_string(n))
Expand Down Expand Up @@ -82,7 +86,7 @@ def setUp(self):

global OSS_AUTH_VERSION
OSS_AUTH_VERSION = os.getenv('OSS_TEST_AUTH_VERSION')

self.bucket = oss2.Bucket(oss2.make_auth(OSS_ID, OSS_SECRET, OSS_AUTH_VERSION), OSS_ENDPOINT, OSS_BUCKET)

try:
Expand Down

0 comments on commit 6317035

Please sign in to comment.