Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add key.delete() #27

Merged
merged 2 commits into from
Jun 15, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.PHONY: test
test:
rm -f .coverage
nosetests --exe --cover-package=s3po --with-coverage --cover-branches --logging-clear-handlers -v
nosetests --rednose --exe --cover-package=s3po --with-coverage --cover-branches --logging-clear-handlers -v

clean:
# Remove the build
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ gevent==1.0.1
mock==1.3.0
nose==1.3.4
python_swiftclient==2.7.0
rednose==1.1.1
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')