Skip to content

Commit

Permalink
Add key.delete()
Browse files Browse the repository at this point in the history
  • Loading branch information
vadim suvorov committed Jun 15, 2016
1 parent cc1ea78 commit 8a94857
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 12 deletions.
10 changes: 9 additions & 1 deletion s3po/backends/memory.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'''An in-memory backend'''

from ..exceptions import DownloadException
import collections

from ..exceptions import DownloadException, DeleteException


class Memory(object):
'''An in-memory backend'''
Expand Down Expand Up @@ -31,3 +32,10 @@ def list(self, bucket, prefix=None, delimiter=None, retries=None, headers=None):
return (prefix for prefix in set(key.split(delimiter, 1)[0] for key in keys))
else:
return keys

def delete(self, bucket, key, retries, headers=None):
'''Delete bucket/key with headers'''
if key in self.buckets[bucket]:
del self.buckets[bucket][key]
else:
raise DeleteException('Failed to delete %s/%s' % (bucket, key))
22 changes: 20 additions & 2 deletions s3po/backends/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@


from boto.s3.connection import S3Connection
from boto.exception import S3ResponseError
from boto.exception import S3ResponseError, BotoServerError, BotoClientError
from cStringIO import StringIO

from ..util import CountFile, retry, logger
from ..exceptions import UploadException, DownloadException
from ..exceptions import UploadException, DownloadException, DeleteException


class S3(object):
Expand Down Expand Up @@ -107,3 +107,21 @@ def list(self, bucket, prefix=None, delimiter=None, retries=3, headers=None):
# consume iterator to make a list to keep parity with Swift backend
return (key.name for key in self._list_retry(retries, bucket,
prefix, delimiter, headers=headers))

def delete(self, bucket, key, retries, headers=None):
'''Delete bucket/key with headers'''
# Make our headers object
headers = headers or {}
bucket = self.get_bucket(bucket)
key = bucket.new_key(key)

@retry(retries)
def func():
'''The bit that we want to retry'''
try:
key.delete(headers=headers)
except (BotoServerError, BotoClientError) as exc:
raise DeleteException('Failed to delete %s/%s: %s(%s)' %
(bucket, key, exc.__class__.__name__, exc.message))

return func()
16 changes: 15 additions & 1 deletion s3po/backends/swift.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from swiftclient.exceptions import ClientException

from ..util import CountFile, retry, logger
from ..exceptions import UploadException, DownloadException
from ..exceptions import UploadException, DownloadException, DeleteException


class Swift(object):
Expand Down Expand Up @@ -90,3 +90,17 @@ def list(self, bucket, prefix=None, delimiter=None, retries=3,
delimiter=delimiter,
marker=marker,
limit=chunksize)[1]

def delete(self, bucket, key, retries, headers=None):
'''Delete bucket/key with headers'''
# Make our headers object
headers = headers or {}

@retry(retries)
def func():
try:
self.conn.delete_object(bucket, key, headers=headers)
except ClientException:
raise DeleteException('Failed to delete %s/%s' % (bucket, key))

func()
5 changes: 5 additions & 0 deletions s3po/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,8 @@ def download_file(self, bucket, key, path, headers=None, retries=3, mode='w'):
def list(self, bucket, prefix=None, delimiter=None, retries=3, headers=None):
'''List the contents of the bucket, optionally specifying a prefix.'''
return self.backend.list(bucket, prefix, delimiter, retries, headers)

def delete(self, bucket, key, headers=None, retries=3):
'''Delete the bucket/key'''
logger.info('Deleting %s / %s', bucket, key)
return self.backend.delete(bucket, key, retries, headers)
5 changes: 5 additions & 0 deletions s3po/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ class DownloadException(S3POException):
class UploadException(S3POException):
'''An error while uploading'''
pass


class DeleteException(S3POException):
'''An error while deleting'''
pass
32 changes: 29 additions & 3 deletions test/test_backends/test_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from base import BaseTest

from s3po.backends.s3 import S3
from s3po.exceptions import UploadException, DownloadException
from s3po.exceptions import UploadException, DownloadException, DeleteException


class S3BackendTest(BaseTest):
Expand Down Expand Up @@ -78,6 +78,24 @@ def test_list_prefix(self):
['key'])


def test_delete(self):
'''Can delete a key'''
self.bucket.new_key('key')
with mock.patch.object(self.backend.conn, 'get_bucket', return_value=self.bucket):
self.backend.delete('bucket', 'key', 1)
self.assertEqual(list(self.backend.list('bucket', prefix='k')),
[])

def test_deletion_error(self):
'''Raises DeleteException on key.delete() error'''
key = self.bucket.new_key('key')
exception = S3ResponseError(404, 'Not Found')
with mock.patch.object(self.backend.conn, 'get_bucket', return_value=self.bucket):
with mock.patch.object(key, 'delete', side_effect=exception):
with self.assertRaises(DeleteException):
self.backend.delete('bucket', 'key', 1)


class Bucket(object):
'''A mock bucket.'''

Expand All @@ -90,9 +108,12 @@ def get_key(self, key, validate=True):

def new_key(self, key):
if key not in self.keys:
self.keys[key] = Key()
self.keys[key] = Key(self, key)
return self.keys[key]

def delete_key(self, key):
del self.keys[key]

def list(self, prefix, delimiter, headers=None):
prefix = prefix or ''
Key = namedtuple('Key', ['name'])
Expand All @@ -104,9 +125,11 @@ def initiate_multipart_upload(self, key, headers=None):

class Key(object):
'''S3 key'''
def __init__(self):
def __init__(self, bucket, key):
self.data = ''
self.headers = {}
self.bucket = bucket
self.key = key

@property
def size(self):
Expand All @@ -119,6 +142,9 @@ def set_contents_from_string(self, data, headers=None):
def get_contents_to_file(self, fobj, headers=None):
fobj.write(self.data)

def delete(self, headers=None):
self.bucket.delete_key(self.key)


class Multi(object):
'''Multipart upload.'''
Expand Down
8 changes: 7 additions & 1 deletion test/test_backends/test_swift.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from base import BaseTest

from s3po.backends.swift import Swift
from s3po.exceptions import UploadException, DownloadException
from s3po.exceptions import UploadException, DownloadException, DeleteException


class SwiftBackendTest(BaseTest):
Expand Down Expand Up @@ -68,3 +68,9 @@ def test_list(self):
(None, [])]
self.assertEqual(list(self.backend.list('bucket')),
['key'])

def test_delete(self):
'''Raises DeleteException when Swift raises.'''
self.conn.delete_object.side_effect = ClientException('Failed to delete')
with self.assertRaises(DeleteException):
self.backend.delete('bucket', 'key', 1)
18 changes: 14 additions & 4 deletions test/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from base import BaseTest
from cStringIO import StringIO

from s3po.exceptions import DownloadException
from s3po.exceptions import DownloadException, DeleteException


class ConnectionTest(BaseTest):
Expand Down Expand Up @@ -61,18 +61,28 @@ def test_mock(self):

def test_list(self):
self.conn.upload('bucket', 'key', StringIO('content'))
self.assertEqual(list(self.conn.list('bucket')),
self.assertEqual(list(self.conn.list('bucket')),
['key'])

def test_list_prefix(self):
self.conn.upload('bucket', 'key', StringIO('content'))
self.conn.upload('bucket', 'something_else', StringIO('content'))
self.assertEqual(list(self.conn.list('bucket', prefix='k')),
self.assertEqual(list(self.conn.list('bucket', prefix='k')),
['key'])

def test_list_delimiter(self):
self.conn.upload('bucket', 'a.1', StringIO('content'))
self.conn.upload('bucket', 'a.2', StringIO('content'))
self.assertEqual(list(self.conn.list('bucket', delimiter='.')),
self.assertEqual(list(self.conn.list('bucket', delimiter='.')),
['a'])

def test_delete_file(self):
'''Can delete.'''
self.conn.upload('bucket', 'key', 'content')
self.conn.delete('bucket', 'key')
self.assertEqual(list(self.conn.list('bucket', prefix='k')), [])

def test_delete_unexisting_key(self):
'''Delete raises when key not found.'''
with self.assertRaises(DeleteException):
self.conn.delete('bucket', 'key')

0 comments on commit 8a94857

Please sign in to comment.