Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #8 from sudorandom/pep8/compat

PEP8 Fixes, Python 2.4, 2.5, 2.6, 2.7, 3.2, Pypy Support, Tox Config Addition
  • Loading branch information...
commit 78812ab43af15b1907b3c9c85ae4328d60c862cf 2 parents f9271f2 + 8ec5dca
@sudorandom sudorandom authored
View
22 object_storage/__init__.py
@@ -1,10 +1,12 @@
"""
SoftLayer Object Storage python client.
-
+
See COPYING for license information
"""
-from object_storage.client import Client
-from object_storage.consts import __version__
+import object_storage.consts
+
+__version__ = object_storage.consts.__version__
+
def get_client(*args, **kwargs):
""" Returns an Object Storage client (using httplib2)
@@ -13,10 +15,11 @@ def get_client(*args, **kwargs):
@param password: password or api key for Object Storage
@param auth_url: Auth URL for Object Storage
@param auth_token: If provided, bypasses authentication and uses the given auth_token
- @return: `object_storage.client.Client`
+ @return: `object_storage.client.Client`
"""
return get_httplib2_client(*args, **kwargs)
+
def get_httplib2_client(username, password, auth_url=None, auth_token=None, **kwargs):
""" Returns an Object Storage client (using httplib2)
@@ -24,8 +27,9 @@ def get_httplib2_client(username, password, auth_url=None, auth_token=None, **kw
@param password: password or api key for Object Storage
@param auth_url: Auth URL for Object Storage
@param auth_token: If provided, bypasses authentication and uses the given auth_token
- @return: `object_storage.client.Client`
+ @return: `object_storage.client.Client`
"""
+ from object_storage.client import Client
from object_storage.transport.httplib2conn import AuthenticatedConnection, Authentication
auth = Authentication(username, password, auth_url=auth_url, auth_token=auth_token, **kwargs)
@@ -33,8 +37,10 @@ def get_httplib2_client(username, password, auth_url=None, auth_token=None, **kw
client = Client(username, password, connection=conn)
return client
+
def get_requests_client(username, password, auth_url=None, auth_token=None, **kwargs):
""" Returns an Object Storage client (using Requests) """
+ from object_storage.client import Client
from object_storage.transport.requestsconn import AuthenticatedConnection, Authentication
auth = Authentication(username, password, auth_url=auth_url, auth_token=auth_token, **kwargs)
@@ -42,13 +48,17 @@ def get_requests_client(username, password, auth_url=None, auth_token=None, **kw
client = Client(username, password, connection=conn)
return client
+
def get_twisted_client(username, password, auth_url=None, auth_token=None, **kwargs):
""" Returns an Object Storage client (using Twisted) """
+ from object_storage.client import Client
from object_storage.transport.twist import AuthenticatedConnection, Authentication
auth = Authentication(username, password, auth_url=auth_url, auth_token=auth_token, **kwargs)
conn = AuthenticatedConnection(auth)
client = Client(username, password, connection=conn)
-
+
d = conn.authenticate().addCallback(lambda r: client)
return d
+
+__all__ = ['get_client', 'get_httplib2_client', 'get_requests_client', 'get_twisted_client']
View
85 object_storage/client.py
@@ -3,10 +3,7 @@
See COPYING for license information.
"""
-try:
- import simplejson as json
-except ImportError:
- import json
+from object_storage.utils import json, Model
from object_storage.container import Container
from object_storage.storage_object import StorageObject
@@ -15,10 +12,10 @@
from object_storage import errors
import logging
-import UserDict
logger = logging.getLogger(__name__)
-class AccountModel(UserDict.UserDict):
+
+class AccountModel(Model):
def __init__(self, controller, headers={}):
_headers = {}
@@ -30,7 +27,7 @@ def __init__(self, controller, headers={}):
self._meta = None
_properties = {}
-
+
_properties['container_count'] = int(self.headers.get('x-account-container-count') or\
self.headers.get('count') or 0)
_properties['object_count'] = int(self.headers.get('x-account-object-count') or\
@@ -53,22 +50,23 @@ def __init__(self, controller, headers={}):
self.properties = _properties
self.data = self.properties
+
class Client(object):
"""
Client class. Primary interface for the client.
"""
- def __init__(self, username=None,
- api_key=None,
- connection=None,
+ def __init__(self, username=None,
+ api_key=None,
+ connection=None,
delimiter='/',
**kwargs):
""" constructor for Client object
@param username: the username
@param api_key: api_key for Object Storage
- @param connection: `object_storage.transport.AuthenticatedConnection`
+ @param connection: `object_storage.transport.AuthenticatedConnection`
instance.
- @param delimiter: the symbol to use to divid up hiearchical divisions
+ @param delimiter: the symbol to use to divid up hiearchical divisions
for objects.
@param container_class: factory or class for Container constructing
@param object_class: factory or class for StorageObject constructing
@@ -84,7 +82,7 @@ def __init__(self, username=None,
self.model = None
def load(self, cdn=True):
- """ load data for the account
+ """ load data for the account
@return: object_storage.client, self
"""
@@ -134,15 +132,15 @@ def is_dir(self):
return True
def search(self, q, options=None, **kwargs):
- """ Access the search interface.
+ """ Access the search interface.
@param q: the search query. This can be None.
@param options: options for the search API. Valid options:
q.[fieldname] -> define search query for a specific field.
field -> field name (when using q)
type -> 'object' or 'container'; default shows both.
- recursive -> whether to search recursively or to limit to
+ recursive -> whether to search recursively or to limit to
one level; default=true
- @param **kwargs: to be merged into the options param.
+ @param **kwargs: to be merged into the options param.
Provides a nicer interface for the same thing.
More information on options:
@@ -163,10 +161,11 @@ def search(self, q, options=None, **kwargs):
params = dict(default_params.items() + params.items())
headers = {'X-Context': 'search'}
_path = None
- if options.has_key('container'):
+ if 'container' in options:
_path = [options['container']]
if 'path' in options and type(options['path']) is not dict:
_path = [options['path']]
+
def _formatter(response):
""" Formats search results. """
headers = response.headers
@@ -184,18 +183,18 @@ def _formatter(response):
count = int(headers.get('x-search-items-count', 0))
total = int(headers.get('x-search-items-total', 0))
return {'count': count, 'total': total, 'results': objs}
- return self.make_request('GET', _path, headers=headers,
+ return self.make_request('GET', _path, headers=headers,
params=params,
formatter=_formatter)
def set_delimiter(self, delimiter):
- """ Sets the delimiter for pseudo hierarchical directory structure.
+ """ Sets the delimiter for pseudo hierarchical directory structure.
@param delimiter: delimiter to use
"""
self.delimiter = delimiter
def set_storage_url(self, url):
- """ Sets the storage URL. After authentication, the URL is automatically
+ """ Sets the storage URL. After authentication, the URL is automatically
populated, but the default value can be overwritten.
@param url: url to use to call the Object Storage API.
@@ -203,15 +202,15 @@ def set_storage_url(self, url):
self.storage_url = url
def container(self, name, headers=None):
- """ Initializes container object.
-
+ """ Initializes container object.
+
@param name: name of the container
@param headers: initial headers to use to initialize the object
"""
return self.container_class(name, headers=headers, client=self)
def get_container(self, name):
- """ Makes a container object and calls load() on it.
+ """ Makes a container object and calls load() on it.
@param name: container name
@raises ResponseError
"""
@@ -227,12 +226,12 @@ def set_metadata(self, meta, headers={}):
for k, v in headers.iteritems():
meta_headers[k] = v
for k, v in meta.iteritems():
- meta_headers["x-account-meta-{0}".format(k)] = v
+ meta_headers["x-account-meta-%s" % (k, )] = v
self.make_request('POST', headers=meta_headers)
-
+
def create_container(self, name):
""" Creates a new container
-
+
@param name: container name
@raises ResponseError
"""
@@ -240,7 +239,7 @@ def create_container(self, name):
def delete_container(self, name, recursive=False):
""" Deletes a container.
-
+
@param name: container name
@raises ResponseError
@raises ContainerNotEmpty if container is not empty
@@ -248,16 +247,17 @@ def delete_container(self, name, recursive=False):
params = {}
if recursive:
params['recursive'] = True
+
def _formatter(res):
if res.status_code is 409:
raise errors.ContainerNotEmpty(name)
return True
return self.make_request('DELETE', [name], params=params, formatter=_formatter)
-
+
def containers(self, marker=None, headers=None):
""" Lists containers
-
+
@param marker: start listing after this container name
@param headers: extra headers to use when making the listing call
@raises ResponseError
@@ -265,6 +265,7 @@ def containers(self, marker=None, headers=None):
params = {'format': 'json'}
if marker:
params['marker'] = marker
+
def _formatter(res):
containers = []
if res.content:
@@ -285,26 +286,26 @@ def public_containers(self, *args, **kwargs):
def storage_object(self, container, name, headers=None):
""" Initialize a StorageObject instance
-
+
@param container: container name
@param name: object name
@param headers: initial headers to use to initialize the object
"""
- return self.object_class(container, name,
+ return self.object_class(container, name,
headers=headers, client=self)
-
+
def get_object(self, container, name):
""" Load an object from swift
-
+
@param container: container name
@param name: object name
@raises ResponseError
"""
return self.storage_object(container, name).load()
-
+
def delete_object(self, container, name):
""" Delete an object from swift
-
+
@param container: container name
@param name: object name
@raises ResponseError
@@ -313,7 +314,7 @@ def delete_object(self, container, name):
def get_url(self, path=None):
""" Returns the url of the resource
-
+
@param path: path to append to the end of the URL
"""
url = self.storage_url
@@ -326,7 +327,7 @@ def get_url(self, path=None):
def make_request(self, method, path=None, *args, **kwargs):
""" Make an HTTP request
-
+
@param method: HTTP method (GET, HEAD, POST, PUT, ...)
@param path: path
@raises ResponseError
@@ -335,9 +336,9 @@ def make_request(self, method, path=None, *args, **kwargs):
result = self.conn.make_request(method, url, *args, **kwargs)
return result
- def chunk_download(self, path, chunk_size=10*1024, headers=None):
+ def chunk_download(self, path, chunk_size=10 * 1024, headers=None):
""" Returns a chunk download generator
-
+
@param path: path
@param chunk_size: the max size in bytes to return on each yield
@param headers: extra headers to use with this request
@@ -346,15 +347,15 @@ def chunk_download(self, path, chunk_size=10*1024, headers=None):
url = self.get_url(path)
return self.conn.chunk_download(url, chunk_size=chunk_size)
- def chunk_upload(self, path, headers=None):
+ def chunk_upload(self, path, size=None, headers=None):
""" Returns a chunkable connection object at the given path
-
+
@param path: path
@param headers: extra headers to use with this request
@raises ResponseError
"""
url = self.get_url(path)
- return self.conn.chunk_upload('PUT', url, headers)
+ return self.conn.chunk_upload('PUT', url, size=size, headers=headers)
def __getitem__(self, name):
""" Returns a container object with the given name """
View
4 object_storage/consts.py
@@ -1,5 +1,5 @@
-"""
- Commonly used constants
+"""
+ Commonly used constants
See COPYING for license information
"""
View
56 object_storage/container.py
@@ -3,14 +3,14 @@
See COPYING for license information
"""
-import json
import os
-import UserDict
+from object_storage.utils import json, Model
from object_storage import errors
from object_storage.storage_object import StorageObject
from object_storage.utils import get_path
-class ContainerModel(UserDict.UserDict):
+
+class ContainerModel(Model):
def __init__(self, controller, name, headers={}):
self.name = name
_headers = {}
@@ -54,9 +54,10 @@ def __init__(self, controller, name, headers={}):
self.properties = _properties
self.data = self.properties
+
class Container:
""" Container class. Encapsulates Storage containers. """
- def __init__(self, name, headers=None, client=None):
+ def __init__(self, name, headers=None, client=None):
""" constructor for Container
@param name: container name
@@ -92,6 +93,7 @@ def load(self, cdn=True):
headers = {}
if cdn:
headers.setdefault('X-Context', 'cdn')
+
def _formatter(res):
self.model = ContainerModel(self, self.name, res.headers)
return self
@@ -147,12 +149,12 @@ def set_metadata(self, meta):
"""
meta_headers = {}
for k, v in meta.iteritems():
- meta_headers["x-container-meta-{0}".format(k)] = v
+ meta_headers["x-container-meta-%s" % (k, )] = v
return self.make_request('POST', headers=meta_headers)
def create(self):
""" Create container
-
+
@raises ResponseError
@return: Containert - self
"""
@@ -162,27 +164,27 @@ def _formatter(res):
def delete(self, recursive=False):
""" Delete container
-
- @param recursive: true if you want to delete all of the
+
+ @param recursive: true if you want to delete all of the
objects in the container as well.
@raises ResponseError
@return: True
"""
return self.client.delete_container(self.name, recursive=recursive)
-
+
def delete_all_objects(self):
""" Deletes all objects in the container
-
+
@raises ResponseError
"""
resps = []
for item in self.objects():
resps.append(item.delete())
return resps
-
+
def delete_object(self, obj):
""" Deletes an object in the container
-
+
@param obj: object name to delete
@raises ResponseError
"""
@@ -192,7 +194,7 @@ def delete_object(self, obj):
def rename(self, new_container):
""" Rename container. Will not work if container is not empty.
-
+
@param new_container: new container name
@raises ResponseError
"""
@@ -201,10 +203,10 @@ def rename(self, new_container):
def objects(self, limit=None, marker=None, base_only=False, headers=None):
""" Lists objects in the container.
-
+
@param limit: limit of results to return.
@param marker: start listing after this object name
- @param base_only: only return the base objects.
+ @param base_only: only return the base objects.
container/object not container/dir/object
@param headers: extra headers to use in the request
@raises ResponseError
@@ -222,7 +224,7 @@ def _formatter(res):
objects = {}
if res.content:
items = json.loads(res.content)
- for item in items:
+ for item in items:
if 'name' in item:
objects[item['name']] = self.storage_object(item['name'], item)
elif 'subdir' in item:
@@ -234,7 +236,7 @@ def _formatter(res):
def set_ttl(self, ttl):
""" Set time to live for CDN
-
+
@param ttl: time in seconds to set as the TTL
@raises ResponseError
"""
@@ -245,16 +247,16 @@ def set_ttl(self, ttl):
def set_read_acl(self, acl):
""" Set read ACL
-
+
@param acl: ACL to set for the container
@raises ResponseError
"""
headers = {'x-container-read': acl}
return self.make_request('POST', headers=headers)
-
+
def set_write_acl(self, acl):
""" Set write ACL
-
+
@param acl: ACL to set for the container
@raises ResponseError
"""
@@ -263,7 +265,7 @@ def set_write_acl(self, acl):
def make_public(self, ttl=1440):
""" Make container public
-
+
@param ttl: time in seconds to set as the TTL
@raises ResponseError
"""
@@ -273,7 +275,7 @@ def make_public(self, ttl=1440):
def make_private(self):
""" Make container private (empty ACL)
-
+
@raises ResponseError
"""
headers = {'x-container-read': ' '}
@@ -293,7 +295,7 @@ def get_object(self, name):
def storage_object(self, name, headers=None):
""" Creates a new instance of Object """
return self.client.storage_object(self.name, name, headers=headers)
-
+
def load_from_filename(self, filename):
""" Creates an object from a file. Uses the basename of the file path as the object name. """
name = os.path.basename(filename)
@@ -303,16 +305,16 @@ def make_request(self, method, path=None, *args, **kwargs):
""" Makes a request on the resource. """
path = [self.name]
return self.client.make_request(method, path, *args, **kwargs)
-
+
def __getitem__(self, name):
""" Returns object corresponding to the given name """
return self.storage_object(name)
-
+
def __str__(self):
return self.name
-
+
def __repr__(self):
- return 'Container({0})'.format(self.name.encode("utf-8"))
+ return 'Container(%s)' % (self.name.encode("utf-8"), )
def __iter__(self):
""" Returns an interator based on results of self.objects() """
View
31 object_storage/errors.py
@@ -1,41 +1,51 @@
-"""
- Exceptions
+"""
+ Exceptions
See COPYING for license information
"""
-class ObjectStorageError(StandardError):
+
+
+class ObjectStorageError(Exception):
""" A general Object Storage error. """
pass
-
+
+
class AuthenticationError(ObjectStorageError):
""" Could not authenticate. """
pass
+
class StorageURLNotFound(AuthenticationError):
- """
+ """
Raised when the requested protocol/network-type not found in Authentication response.
"""
pass
-
+
+
class ContainerExists(ObjectStorageError):
""" Container already exists """
pass
-
+
+
class ContainerNotEmpty(ObjectStorageError):
""" Container is not empty """
pass
+
class NotFound(ObjectStorageError):
""" Resource not found """
-
+
+
class ObjectNotFound(NotFound):
""" Object not found """
pass
-
+
+
class ContainerNotFound(NotFound):
""" Container not found """
pass
-
+
+
class ResponseError(ObjectStorageError):
""" Response error """
def __init__(self, status, reason):
@@ -48,4 +58,3 @@ def __str__(self):
def __repr__(self):
return '%d: %s' % (self.status, self.reason)
-
View
155 object_storage/storage_object.py
@@ -3,16 +3,23 @@
See COPYING for license information
"""
-import json
+from object_storage.utils import json, Model
import mimetypes
import os
-import StringIO
-import UserDict
+try:
+ import StringIO
+except ImportError:
+ from io import StringIO as StringIO
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
from object_storage import errors
from object_storage.utils import get_path
-class StorageObjectModel(UserDict.UserDict):
+
+class StorageObjectModel(Model):
def __init__(self, controller, container, name, headers={}):
self.container = container
self.name = name
@@ -24,7 +31,7 @@ def __init__(self, controller, container, name, headers={}):
_headers[_key] = value
self.headers = _headers
self._meta = None
-
+
_properties = {'container': self.container, 'name': self.name}
_properties['size'] = int(self.headers.get('content-length') or\
@@ -58,11 +65,13 @@ def __init__(self, controller, container, name, headers={}):
self.properties = _properties
self.data = self.properties
+
class StorageObject:
- """
+ """
Representation of a Object Storage object.
"""
- chunk_size=10*1024
+ chunk_size = 10 * 1024
+
def __init__(self, container, name, headers=None, client=None):
""" constructor for StorageObject
@@ -89,7 +98,7 @@ def _formatter(res):
self.model = StorageObjectModel(self, self.container, self.name, res.headers)
return True
try:
- return self.make_request('HEAD', headers={'X-Context': 'cdn'}, formatter=_formatter)
+ return self.make_request('HEAD', formatter=_formatter)
except errors.NotFound:
return False
@@ -102,6 +111,7 @@ def load(self, cdn=True):
headers = {}
if cdn:
headers.setdefault('X-Context', 'cdn')
+
def _formatter(res):
self.model = StorageObjectModel(self, self.container, self.name, res.headers)
return self
@@ -144,23 +154,24 @@ def path(self):
""" Get the path of the object """
path = [self.container, self.name]
return get_path(path)
-
+
def list(self, limit=None, marker=None, base_only=False):
""" Uses sudo-hierarchical structure to list the children objects.
-
+
@param limit: limit of results to return.
@param marker: start listing after this object name
@raises ResponseError
@return: list of StorageObject instances
"""
params = {'format': 'json',
- 'prefix': self.name+self.client.delimiter}
+ 'prefix': self.name + self.client.delimiter}
if base_only:
params['delimiter'] = self.client.delimiter
if limit:
params['limit'] = limit
if marker:
params['marker'] = marker
+
def _formatter(res):
objects = {}
if res.content:
@@ -174,8 +185,8 @@ def _formatter(res):
item['content_type'] = 'application/directory'
objects[item['name']] = self.client.storage_object(self.container, item['name'], headers=item)
return objects.values()
- return self.client.make_request('GET', [self.container], params=params, formatter=_formatter)
-
+ return self.client.make_request('GET', [self.container], params=params, formatter=_formatter)
+
def is_dir(self):
""" returns True if content_type is 'text/directory' or 'application/directory' """
return self.model.content_type in ['text/directory', 'application/directory']
@@ -188,12 +199,12 @@ def set_metadata(self, meta):
"""
meta_headers = {}
for k, v in meta.iteritems():
- meta_headers["X-Object-Meta-{0}".format(k)] = v
+ meta_headers["X-Object-Meta-%s" % (k, )] = v
return self.make_request('POST', headers=meta_headers)
def create(self):
""" Create object
-
+
@raises ResponseError
@return: StorageObject - self
"""
@@ -201,37 +212,46 @@ def create(self):
if not content_type:
content_type = 'application/octet-stream'
headers = {'content-type': content_type, 'Content-Length': '0'}
+
def _formatter(res):
return self
return self.make_request('PUT', headers=headers, formatter=_formatter)
-
+
def delete(self, recursive=False):
""" Delete object
-
+
@raises ResponseError
@return: True
"""
return self.client.delete_object(self.container, self.name)
-
- def read(self, size=0, offset=0, headers=None):
+
+ def read(self, size=None, offset=None, headers=None):
""" Reads object content
-
+
@param size: number of bytes to read (0 reads all of the object data)
@param offset: number of bytes to offset the read
@raises ResponseError
@return: str, data
"""
headers = headers or {}
- if size > 0:
- _range = 'bytes=%d-%d' % (offset, (offset + size) - 1)
- headers['Range'] = _range
+ if all([offset, size]):
+ end = (offset + size) - 1
+ headers['Range'] = 'bytes=%s-%s' % (offset, end)
+ elif offset is None and size is not None and size < 0:
+ headers['Range'] = 'bytes=%s' % (size,)
+ elif offset is None and size:
+ end = size - 1
+ headers['Range'] = 'bytes=0-%s' % (end,)
+ elif offset is not None and size is None:
+ headers['Range'] = 'bytes=%s-' % (offset,)
+
def _formatter(res):
return res.content
return self.make_request('GET', headers=headers, formatter=_formatter)
def save_to_filename(self, filename):
""" Reads object content into a file
-
+
@param filename: filename
@raises ResponseError
"""
@@ -242,11 +262,11 @@ def save_to_filename(self, filename):
f.write(data)
finally:
f.close()
-
+
def chunk_download(self, chunk_size=None):
""" Returns an iterator to read the object data.
-
- @param chunk_size: size of the chunks to read in.
+
+ @param chunk_size: size of the chunks to read in.
If not defined uses self.chunk_size
@raises: ResponseError
@return: iterable
@@ -255,22 +275,23 @@ def chunk_download(self, chunk_size=None):
return self.client.chunk_download([self.container, self.name], chunk_size=chunk_size)
iter_content = chunk_download
__iter__ = chunk_download
-
- def chunk_upload(self, headers=None):
+
+ def chunk_upload(self, size=None, headers=None):
""" Returns a chunkable upload instance.
This is needed for transient data uploads
-
+
+ @param headers: size in bytes, if known
@param headers: extra headers to use to initialize the request
@raises: ResponseError
- @return: object that responds to o.send('data') to send data
+ @return: object that responds to o.send('data') to send data
and o.finish() to finish the upload.
"""
- chunkable = self.client.chunk_upload([self.container, self.name], headers=headers)
+ chunkable = self.client.chunk_upload([self.container, self.name], size=size, headers=headers)
return chunkable
-
- def send(self, data):
+
+ def send(self, data, check_md5=True):
""" Uploads object data
-
+
@param data: either a file-like object or a string.
@raises: ResponseError
@return: StorageObject, self
@@ -286,6 +307,9 @@ def send(self, data):
if hasattr(data, '__len__'):
size = len(data)
+ if isinstance(data, basestring):
+ data = StringIO.StringIO(data)
+
headers = {}
content_type = self.content_type
if not content_type:
@@ -295,17 +319,30 @@ def send(self, data):
content_type = _type or mimetypes.guess_type(self.name)[0] or 'application/octet-stream'
headers['Content-Type'] = content_type
- if size or size == 0:
- headers['Content-Length'] = str(size)
- else:
- headers['Transfer-Encoding'] = 'chunked'
+ checksum = md5()
+ transfered = 0
+ conn = self.chunk_upload(size=size, headers=headers)
+ buff = data.read(4096)
+ while len(buff) > 0:
+ conn.send(buff)
+ if check_md5:
+ checksum.update(buff)
+ transfered += len(buff)
+ buff = data.read(4096)
+ res = conn.finish()
+
+ if check_md5:
+ assert checksum.hexdigest() == res.headers['etag'], 'md5 hashes do not match'
+ res.headers['content-length'] = transfered
+ self.model = StorageObjectModel(self, self.container, self.name, res.headers)
+ headers['Content-Type'] = content_type
+ return self
- return self.make_request('PUT', data=data, headers=headers, formatter=lambda r: self)
write = send
-
+
def upload_directory(self, directory):
""" Uploads an entire directory
-
+
@param directory: path of the directory to upload
@raises: ResponseError
"""
@@ -316,31 +353,33 @@ def upload_directory(self, directory):
directories.append(os.path.relpath(os.path.join(root, _dir)))
for _file in filenames:
files.append(os.path.relpath(os.path.join(root, _file)))
-
+
for _dir in directories:
obj = self.__class__(self.container, _dir, client=self.client)
obj.content_type = 'application/directory'
obj.create()
-
+
for _file in files:
obj = self.__class__(self.container, _file, client=self.client)
obj.load_from_filename(_file)
-
+
def load_from_filename(self, filename):
""" Uploads a file from the local filename
-
+
@param filename: path of the directory to upload
@raises: ResponseError, IOError
"""
if os.path.isdir(filename):
self.upload_directory(filename)
else:
- with open(filename, 'rb') as _file:
+ try:
+ _file = open(filename, 'rb')
+ finally:
return self.send(_file)
def copy_from(self, old_obj, *args, **kwargs):
""" Copies content from an existing object
-
+
@param old_obj: StorageObject instance to copy data from
@raises: ResponseError
@return: StorageObject, self
@@ -349,12 +388,12 @@ def copy_from(self, old_obj, *args, **kwargs):
headers['X-Copy-From'] = old_obj.path
headers['Content-Length'] = "0"
if 'formatter' not in kwargs:
- kwargs['formatter'] = lambda r: new_obj
+ kwargs['formatter'] = lambda r: self
return self.make_request('PUT', headers=headers, *args, **kwargs)
def copy_to(self, new_obj, *args, **kwargs):
""" Copies content from an existing object
-
+
@param new_obj: StorageObject instance to copy data to
@raises: ResponseError
@return: StorageObject, new_obj
@@ -368,14 +407,16 @@ def copy_to(self, new_obj, *args, **kwargs):
def rename(self, new_obj, *args, **kwargs):
""" Copies content to a new object existing object and deletes the current object
-
+
@param new_obj: StorageObject instance to copy data to
@raises: ResponseError
"""
def _delete(res):
return self.delete()
+
def _copy_to(res):
- return new_obj.copy_from(self, *args, formatter=_delete, **kwargs)
+ kwargs['formatter'] = _delete
+ return new_obj.copy_from(self, *args, **kwargs)
return new_obj.make_request('PUT', headers={'Content-Length': '0'}, formatter=_copy_to)
def search(self, q, options=None, **kwargs):
@@ -384,12 +425,12 @@ def search(self, q, options=None, **kwargs):
options.update({'path': "%s/%s" % (self.container, self.name)})
return self.client.search(q, options=options, **kwargs)
- def prime_cdn(self):
+ def prime_cdn(self, *args, **kwargs):
""" Prime the object for CDN usage """
headers = {'X-Context': 'cdn', 'X-Cdn-Load': True}
return self.make_request('POST', headers=headers, *args, **kwargs)
- def purge_cdn(self):
+ def purge_cdn(self, *args, **kwargs):
""" Purge the object for CDN usage """
headers = {'X-Context': 'cdn', 'X-Cdn-Purge': True}
return self.make_request('POST', headers=headers, *args, **kwargs)
@@ -401,7 +442,7 @@ def make_request(self, method, path=None, *args, **kwargs):
def fileno(self):
return 1
-
+
def __len__(self):
if not self.model:
self.load()
@@ -410,12 +451,12 @@ def __len__(self):
def __getitem__(self, name):
new_name = self.client.delimiter.join([self.name, name])
return self.client.storage_object(self.container, new_name)
-
+
def __str__(self):
size = 'Unknown'
if self.model:
size = self.model.get('size', 0)
- return 'StorageObject({0}, {1}, {2}B)'.format(self.container.encode("utf-8"), self.name.encode("utf-8"), size)
+ return 'StorageObject(%s, %s, %sB)' % (self.container.encode("utf-8"), self.name.encode("utf-8"), size)
__repr__ = __str__
def __enter__(self):
View
81 object_storage/transport/__init__.py
@@ -7,10 +7,12 @@
from socket import timeout
from urlparse import urlparse
from object_storage.errors import ResponseError, NotFound
-from object_storage.utils import unicode_quote
from object_storage import consts
-import urllib, urllib2
+import urllib
+import urllib2
+import re
+
class Response(object):
def __init__(self):
@@ -30,6 +32,7 @@ def raise_for_status(self):
elif (self.status_code >= 500) and (self.status_code < 600):
raise ResponseError(self.status_code, '%s Server Error' % self.status_code)
+
class BaseAuthenticatedConnection:
def _authenticate(self):
""" Do authentication and set token and storage_url """
@@ -41,13 +44,13 @@ def get_headers(self):
""" Get default headers for this connection """
return dict([('User-Agent', consts.USER_AGENT)] + self.auth_headers.items())
- def chunk_upload(self, method, url, headers=None):
+ def chunk_upload(self, method, url, size=None, headers=None):
""" Returns new ChunkedConnection """
headers = headers or {}
headers.update(self.get_headers())
- return ChunkedUploadConnection(self, method, url, headers)
-
- def chunk_download(self, url, chunk_size=10*1024):
+ return ChunkedUploadConnection(self, method, url, size=size, headers=headers)
+
+ def chunk_download(self, url, chunk_size=10 * 1024):
""" Returns new ChunkedConnection """
headers = self.get_headers()
req = urllib2.Request(url)
@@ -60,8 +63,9 @@ def chunk_download(self, url, chunk_size=10*1024):
break
yield buff
+
class BaseAuthentication(object):
- """
+ """
Base Authentication class. To be inherited if you want to create
a new Authentication method. authenticate() should be overwritten.
"""
@@ -88,13 +92,13 @@ def get_storage_url(self, storage_urls):
if self.network in storage_urls:
return storage_urls[self.network]
return None
-
+
@property
def auth_headers(self):
return {'X-Auth-Token': 'AUTH_TOKEN'}
def authenticate(self):
- """
+ """
Called when the client wants to authenticate. self.storage_url and
self.auth_token needs to be set.
"""
@@ -102,61 +106,84 @@ def authenticate(self):
self.auth_token = 'AUTH_TOKEN'
self.authenticated = True
+
class ChunkedUploadConnection:
- """
+ """
Chunked Connection class.
send_chunk() will send more data.
finish() will end the request.
"""
- def __init__(self, conn, method, url, headers=None, size=None):
+ def __init__(self, conn, method, url, size=None, headers=None):
self.conn = conn
self.method = method
self.req = None
+ self._chunked_encoding = True
headers = headers or {}
-
+
if size is None:
- if 'Content-Length' in headers:
- del headers['Content-Length']
headers['Transfer-Encoding'] = 'chunked'
else:
+ self._chunked_encoding = False
headers['Content-Length'] = str(size)
if 'ETag' in headers:
del headers['ETag']
- url_parts = urlparse(url)
- self.req = httplib.HTTPConnection(url_parts.hostname, url_parts.port)
+ scheme, netloc, path, params, query, fragment = urlparse(url)
+ match = re.match('([a-zA-Z0-9\-\.]+):?([0-9]{2,5})?', netloc)
- path = requote_path(url_parts.path)
+ if match:
+ (host, port) = match.groups()
+ else:
+ ValueError('Invalid URL')
+
+ if not port:
+ if scheme == 'https':
+ port = 443
+ else:
+ port = 80
+
+ if scheme == 'https':
+ self.req = httplib.HTTPSConnection(host, port)
+ else:
+ self.req = httplib.HTTPConnection(host, port)
+
+ path = requote_path(path)
try:
self.req.putrequest('PUT', path)
for key, value in headers.iteritems():
self.req.putheader(key, value)
self.req.endheaders()
- except Exception, err:
+ except Exception:
raise ResponseError(0, 'Disconnected')
def send(self, chunk):
""" Sends a chunk of data. """
try:
- self.req.send("%X\r\n" % len(chunk))
- self.req.send(chunk)
- self.req.send("\r\n")
+ if self._chunked_encoding:
+ self.req.send("%X\r\n" % len(chunk))
+ self.req.send(chunk)
+ self.req.send("\r\n")
+ else:
+ self.req.send(chunk)
except timeout, err:
raise err
- except Exception, err:
+ except:
raise ResponseError(0, 'Disconnected')
def finish(self):
""" Finished the request out and receives a response. """
try:
- self.req.send("0\r\n\r\n")
+ if self._chunked_encoding:
+ self.req.send("0\r\n\r\n")
except timeout, err:
raise err
+ except:
+ raise ResponseError(0, 'Disconnected')
res = self.req.getresponse()
content = res.read()
-
+
r = Response()
r.status_code = res.status
r.version = res.version
@@ -164,7 +191,8 @@ def finish(self):
r.content = content
r.raise_for_status()
return r
-
+
+
class ChunkedDownloadConnection:
def __init__(self, conn, method, url, headers=None):
self.conn = conn
@@ -172,7 +200,8 @@ def __init__(self, conn, method, url, headers=None):
self.url = url
self.req = None
self.headers = headers or {}
-
+
+
def requote_path(path):
"""Re-quote the given URL path component.
View
24 object_storage/transport/httplib2conn.py
@@ -3,29 +3,25 @@
See COPYING for license information
"""
-from urlparse import urlparse
import urllib
-from object_storage import consts
from object_storage import errors
from object_storage.transport import BaseAuthentication, BaseAuthenticatedConnection, Response
import httplib2
-try:
- import simplejson as json
-except ImportError:
- import json
+from object_storage.utils import json
import logging
logger = logging.getLogger(__name__)
+
class AuthenticatedConnection(BaseAuthenticatedConnection):
- """
- Connection that will authenticate if it isn't already
+ """
+ Connection that will authenticate if it isn't already
and retry once if an auth error is returned.
"""
def __init__(self, auth, debug=False, **kwargs):
if debug:
- httplib2.debuglevel=4
+ httplib2.debuglevel = 4
self.token = None
self.storage_url = None
self.http = httplib2.Http()
@@ -34,7 +30,7 @@ def __init__(self, auth, debug=False, **kwargs):
if not self.auth.authenticated:
self.auth.authenticate()
self._authenticate()
-
+
def make_request(self, method, url=None, headers=None, formatter=None, params=None, data=None, *args, **kwargs):
""" Makes a request """
headers = headers or {}
@@ -42,6 +38,7 @@ def make_request(self, method, url=None, headers=None, formatter=None, params=No
if params:
url = "%s?%s" % (url, urllib.urlencode(params))
+
def _make_request(headers):
logger.debug("%s %s %s" % (method, url, headers))
res, content = self.http.request(url, method, headers=headers, body=data)
@@ -58,13 +55,14 @@ def _make_request(headers):
self._authenticate()
headers.update(self.auth_headers)
response = _make_request(headers)
-
+
response.raise_for_status()
-
+
if formatter:
return formatter(response)
return response
-
+
+
class Authentication(BaseAuthentication):
"""
Authentication class.
View
30 object_storage/transport/requestsconn.py
@@ -4,24 +4,17 @@
See COPYING for license information
"""
import requests
-import httplib
-from socket import timeout
-from urlparse import urlparse
-from object_storage import consts
from object_storage.transport import BaseAuthentication, BaseAuthenticatedConnection
from object_storage import errors
-
-try:
- import simplejson as json
-except ImportError:
- import json
+from object_storage.utils import json
import logging
logger = logging.getLogger(__name__)
+
class AuthenticatedConnection(BaseAuthenticatedConnection):
- """
- Connection that will authenticate if it isn't already
+ """
+ Connection that will authenticate if it isn't already
and retry once if an auth error is returned.
"""
def __init__(self, auth, **kwargs):
@@ -30,7 +23,7 @@ def __init__(self, auth, **kwargs):
self.auth = auth
self.auth.authenticate()
self._authenticate()
-
+
def make_request(self, method, url=None, *args, **kwargs):
""" Makes a request """
_headers = kwargs.get('headers', {})
@@ -38,7 +31,7 @@ def make_request(self, method, url=None, *args, **kwargs):
if _headers:
headers.update(_headers)
kwargs['headers'] = headers
-
+
if 'verify' not in kwargs:
kwargs['verify'] = False
@@ -51,7 +44,7 @@ def make_request(self, method, url=None, *args, **kwargs):
if kwargs.get('return_response', True):
res = self._check_success(res)
if res.status_code == 404:
- raise NotFound('Not found')
+ raise errors.NotFound('Not found')
if res.error:
try:
raise res.raise_for_status()
@@ -61,10 +54,10 @@ def make_request(self, method, url=None, *args, **kwargs):
if formatter:
return formatter(res)
return res
-
+
def _check_success(self, res):
- """
- Checks for request success. If a 401 is returned, it will
+ """
+ Checks for request success. If a 401 is returned, it will
authenticate again and retry the request.
"""
if res.status_code == 401:
@@ -76,6 +69,7 @@ def _check_success(self, res):
res = res.request.response
return res
+
class Authentication(BaseAuthentication):
"""
Authentication class.
@@ -99,7 +93,7 @@ def authenticate(self):
raise errors.AuthenticationError('Invalid Credentials')
response.raise_for_status()
-
+
try:
storage_options = json.loads(response.content)['storage']
except ValueError:
View
57 object_storage/transport/twist.py
@@ -1,41 +1,35 @@
-"""
- Twisted connection type.
+"""
+ Twisted connection type.
See COPYING for license information
"""
from zope import interface
from object_storage.transport import requote_path
-from object_storage.errors import ResponseError, NotFound
+from object_storage.errors import NotFound
from object_storage.transport import Response, BaseAuthenticatedConnection, BaseAuthentication
-from object_storage import consts
+from object_storage import errors
from twisted.internet import reactor
-from twisted.internet.defer import Deferred, succeed
+from twisted.internet.defer import Deferred
from twisted.internet.protocol import Protocol
from twisted.internet.ssl import ClientContextFactory
from twisted.web.client import Agent
from twisted.web.http_headers import Headers
-from twisted.web.client import FileBodyProducer
from twisted.web.iweb import IBodyProducer, UNKNOWN_LENGTH
-import urlparse, urllib
+import urlparse
+import urllib
-import json
+from object_storage.utils import json
-""":param method: method for the new :class:`Request` object.
- :param url: URL for the new :class:`Request` object.
- :param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`.
- :param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`.
- :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
-"""
def complete_request(resp, callback=None, load_body=True):
r = Response()
r.status_code = resp.code
r.version = resp.version
r.phrase = resp.phrase
- headers = {}
+
for k, v in resp.headers.getAllRawHeaders():
r.headers[k.lower()] = v.pop()
@@ -60,13 +54,15 @@ def build_response(body):
finished.addCallback(build_response)
return finished
+
def print_error(failure):
- from twisted.web import _newclient
+ from twisted.web import _newclient
if failure.check(_newclient.RequestGenerationFailed):
for f in failure.value.reasons:
print f.getTraceback()
return failure
+
class AuthenticatedConnection(BaseAuthenticatedConnection):
def __init__(self, auth, **kwargs):
self.token = None
@@ -83,22 +79,23 @@ def make_request(self, method, url=None, headers=None, *args, **kwargs):
headers.update(self.get_headers())
return make_request(method, url=url, headers=headers, *args, **kwargs)
+
def make_request(method, url=None, headers=None, *args, **kwargs):
""" Makes a request """
- headers = Headers(dict([ (k, [v]) for k, v in headers.items() ]))
-
+ headers = Headers(dict([(k, [v]) for k, v in headers.items()]))
+
formatter = None
if 'formatter' in kwargs:
formatter = kwargs.get('formatter')
del kwargs['formatter']
-
+
if not formatter:
def _nothing(result):
return result
formatter = _nothing
params = kwargs.get('params', None)
-
+
if params:
params = urllib.urlencode(params)
@@ -114,15 +111,16 @@ def _nothing(result):
url,
headers,
body)
-
- load_body=True
+
+ load_body = True
if method.upper() in ['HEAD', 'DELETE']:
- load_body=False
+ load_body = False
d.addCallback(complete_request, formatter, load_body=load_body)
d.addErrback(print_error)
return d
+
def _full_url(url, _params={}):
"""Build the actual URL to use."""
@@ -139,7 +137,7 @@ def _full_url(url, _params={}):
path = requote_path(path)
- url = str(urlparse.urlunparse([ scheme, netloc, path, params, query, fragment ]))
+ url = str(urlparse.urlunparse([scheme, netloc, path, params, query, fragment]))
if _params:
if urlparse.urlparse(url).query:
@@ -149,6 +147,7 @@ def _full_url(url, _params={}):
else:
return url
+
class Authentication(BaseAuthentication):
"""
Authentication class.
@@ -193,10 +192,12 @@ def authenticate(self):
d.addCallback(self._authenticate)
return d
+
class WebClientContextFactory(ClientContextFactory):
def getContext(self, hostname, port):
return ClientContextFactory.getContext(self)
+
class FullBodyReader(Protocol):
def __init__(self, finished):
self.finished = finished
@@ -208,8 +209,9 @@ def dataReceived(self, data):
def connectionLost(self, reason):
self.finished.callback(self.body)
+
class ChunkedConnection:
- """
+ """
Chunked Connection class.
setup() will initiate a HTTP connection.
send_chunk() will send more data.
@@ -225,7 +227,7 @@ def __init__(self, conn, url, headers=None, size=None):
self.body = ChunkedStreamProducer(self.started, self.size)
def setup(self, size=None):
- """
+ """
Sets up the connection. Will optionally accept a size or
else will use a chunked Transfer-Encoding.
"""
@@ -237,7 +239,7 @@ def setup(self, size=None):
req = self.conn.make_request('PUT', self.url, headers=self.headers, data=self.body)
self.req = req
print "ChunkedTwistedConnection: STARTED REQUEST"
-
+
def send_chunk(self, chunk):
""" Sends a chunk of data. """
print "ChunkedTwistedConnection: send chunk"
@@ -247,6 +249,7 @@ def finish(self):
""" Finished the request out and receives a response. """
self.body.finish()
+
class ChunkedStreamProducer(object):
interface.implements(IBodyProducer)
View
54 object_storage/utils.py
@@ -1,22 +1,58 @@
-"""
+"""
Misc Utils
See COPYING for license information
"""
import urllib
+import sys
+
+try:
+ import json
+except ImportError:
+ try:
+ import simplejson as json
+ except ImportError:
+ try:
+ import django.utils.simplejson as json
+ except ImportError:
+ ImportError("Requires a json parsing library")
+
+try:
+ from UserDict import DictMixin
+except ImportError:
+ from collections import MutableMapping as DictMixin
+
+
+class Model(DictMixin):
+ def __getitem__(self, key):
+ return self.properties[key]
-def unicode_quote(s):
- """ Solves an issue with url-quoting unicode strings"""
- if isinstance(s, unicode):
- return urllib.quote(s.encode("utf-8"))
- else:
- return urllib.quote(str(s))
+ def __setitem__(self, key, item):
+ self.properties[key] = item
+
+ def __delitem__(self, key):
+ del self.properties[key]
+
+ def keys(self):
+ return self.properties.keys()
+
+if sys.version_info >= (3,):
+ def unicode_quote(s):
+ from urllib.parse import quote
+ return quote(s)
+else:
+ def unicode_quote(s):
+ """ Solves an issue with url-quoting unicode strings"""
+ if isinstance(s, unicode):
+ return urllib.quote(s.encode("utf-8"))
+ else:
+ return urllib.quote(str(s))
def get_path(parts=None):
- """
- Returns the path to a resource. Parts can be a list of strings or
+ """
+ Returns the path to a resource. Parts can be a list of strings or
a string.
"""
path = parts
View
22 setup.py
@@ -1,9 +1,22 @@
#!/usr/bin/python
from setuptools import setup, find_packages
-from object_storage import __version__
+import sys
+from object_storage.consts import __version__
name = 'softlayer-object-storage'
version = __version__
+_ver = sys.version_info
+
+requirements = ['httplib2']
+if _ver[0] == 2 and _ver[1] < 6:
+ requirements.append('simplejson')
+
+
+# Python 3 conversion
+extra = {}
+if sys.version_info >= (3,):
+ extra['use_2to3'] = True
+
setup(
name=name,
version=version,
@@ -22,9 +35,8 @@
author_email='sldn@softlayer.com',
url='https://github.com/softlayer/softlayer-object-storage-python',
license='MIT',
- packages=find_packages(exclude=['test', 'ez_setup', 'examples']),
test_suite='tests',
- install_requires=['httplib2'],
- setup_requires=['mock'],
- namespace_packages=[]
+ packages=find_packages(exclude=['tests']),
+ install_requires=requirements,
+ **extra
)
View
0  tests/integration/__init__.py
No changes.
View
135 tests/integration/httplib.py
@@ -1,135 +0,0 @@
-import unittest
-import time
-import os
-from object_storage import get_httplib2_client, get_requests_client
-import ConfigParser
-
-class ClientTest(unittest.TestCase):
- container_name = 'python_test_container'
- object_name = 'python_test_object'
-
- def setUp(self):
- config = ConfigParser.ConfigParser()
- config.read(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'user.conf'))
- account = dict(config.items('account'))
- username = account.get('username')
- api_key = account.get('api_key')
- auth_url = account.get('auth_url')
- datacenter = account.get('datacenter')
- network = account.get('network')
- protocol = account.get('protocol')
- self.client = get_httplib2_client(username, api_key,
- auth_url=auth_url,
- protocol=protocol,
- datacenter=datacenter,
- network=network)
-
- def tearDown(self):
- for container in self.client:
- if container.name.startswith('python_test'):
- for obj in container:
- if obj.name.startswith('python_test'):
- obj.delete()
- container.delete()
-
- def filter_by_name(self, lst):
- new_list = []
- for item in lst:
- if item.name.startswith('python_test'):
- new_list.append(item)
- return new_list
-
- def test_auth_setup(self):
- self.client.conn.auth.authenticate()
-
- def test_create_container(self):
- container = self.client[self.container_name].create()
- container = self.client[self.container_name].load()
- self.assert_(container.props['name'] == self.container_name, "Container name matches")
- self.assert_(container.props['count'] == 0, "Container count is 0")
- self.assert_(container.props['size'] == 0.0, "Container size is 0")
-
- def test_create_object(self):
- container = self.client[self.container_name].create()
- obj = container[self.object_name]
- obj.content_type = "text/html"
- obj.create()
- obj.send("<html></html>")
- obj = container[self.object_name].load()
- self.assert_(obj.props['hash'] == 'c83301425b2ad1d496473a5ff3d9ecca', "Hash matches")
- self.assert_(obj.props['content_type'] == "text/html", "Content-type matches")
- self.assert_(obj.props['size'] == 13.0, "Size matches")
- self.assert_(obj.read() == "<html></html>", "Object body matches")
-
- def test_enable_cdn(self):
- container = self.client[self.container_name].create()
- lst = self.client.public_containers()
- container.enable_cdn()
- time.sleep(2)
- lst = self.client.public_containers()
-
- result_list = self.filter_by_name(lst)
- self.assert_(len(result_list) == 1)
- self.assert_(result_list.pop().name == self.container_name, "The container has CDN enabled")
- self.assert_(container.props['read'] == ".r:*", "The container has read property set")
- container.delete()
- time.sleep(2)
- lst = self.client.public_containers()
- result_list = self.filter_by_name(lst)
- self.assert_(len(result_list) == 0)
-
- def test_set_ttl(self):
- container = self.client[self.container_name].create()
- container.set_ttl(1000)
- self.assert_(self.client[self.container_name].load().props['ttl'] == 1000, "TTL is set correctly")
-
- container.set_ttl(None)
- self.assert_(self.client[self.container_name].load().props['ttl'] == 0, "TTL is cleared correctly")
-
- def test_search_container(self):
- time.sleep(2)
- results = self.client.search({'q': '*'})
- result_list = self.filter_by_name(results['results'])
- self.assert_(len(result_list) == 0, "Search returns no results")
-
- container = self.client[self.container_name].create()
- time.sleep(2)
- results = self.client.search('*test*', type='container')
- result_list = self.filter_by_name(results['results'])
- self.assert_(len(result_list) == 1, "Correct amount of search results")
- self.assert_(result_list.pop().name == self.container_name, "Correct search result name")
-
- container.delete()
- time.sleep(2)
- results = self.client.search('*test*', type='container')
- result_list = self.filter_by_name(results['results'])
- self.assert_(len(result_list) == 0, "Search returns no results")
-
- def test_search_object(self):
- time.sleep(2)
- results = self.client.search('*test*')
- result_list = self.filter_by_name(results['results'])
- self.assert_(len(result_list) == 0, "Search returns no results")
-
- container = self.client[self.container_name].create()
- obj = container[self.object_name]
- obj.content_type = 'application/text'
- obj.create()
- time.sleep(3)
-
- results = self.client.search('*test*', type='object')
- result_list = self.filter_by_name(results['results'])
- self.assert_(len(result_list) == 1, "Correct amount of search results")
- self.assert_(result_list.pop().name == self.object_name, "Correct search result name")
-
- results = self.client.search('*test*', type='object', content_type='application/text')
- result_list = self.filter_by_name(results['results'])
- self.assert_(len(result_list) == 1, "Correct amount of search results")
- self.assert_(result_list.pop().name == self.object_name, "Correct search result name")
-
- obj.delete()
- time.sleep(2)
- results = self.client.search('*test*', type='object')
- result_list = self.filter_by_name(results['results'])
- self.assert_(len(result_list) == 0, "Search returns no results")
-
View
1  tests/integration/requests.py
@@ -1 +0,0 @@
-#
View
0  tests/integration/twist.py
No changes.
View
11 tests/unit/authentication.py → tests/test_authentication.py 100755 → 100644
@@ -1,12 +1,16 @@
-import unittest
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
from object_storage.transport import BaseAuthentication
+
class BaseAuthenticationTest(unittest.TestCase):
def test_instance_setup(self):
self.assert_(self.auth.storage_url == None, "Storage url is set correctly")
self.assert_(self.auth.auth_token == None, "auth_token set correctly")
-
+
def test_authenticate(self):
self.auth.authenticate()
self.assert_(self.auth.storage_url == 'STORAGE_URL', "storage_url set correctly")
@@ -14,3 +18,6 @@ def test_authenticate(self):
def setUp(self):
self.auth = BaseAuthentication(auth_url='auth_url')
+
+if __name__ == "__main__":
+ unittest.main()
View
44 tests/unit/client.py → tests/test_client.py 100755 → 100644
@@ -1,7 +1,11 @@
-import unittest
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
from mock import Mock
from object_storage.client import Client
+
class ClientTest(unittest.TestCase):
def test_instance_setup(self):
self.assert_(self.client.username == 'username', "username set")
@@ -10,68 +14,68 @@ def test_instance_setup(self):
self.assert_(self.client.object_class == self.object_class, "object_class set")
self.assert_(self.client.conn == self.connection, "connection set")
self.assert_(self.client.delimiter == '/', "default delimiter set")
-
+
def test_set_delimiter(self):
delimiter = Mock()
self.client.set_delimiter(delimiter)
self.assert_(self.client.delimiter == delimiter, "set_delimiter sets the delimiter")
-
+
def test_container(self):
self.client.container('container')
self.container_class.assert_called_once_with('container', client=self.client, headers=None)
-
+
def test_container_with_props(self):
self.client.container('container', {'properties': 'property'})
self.container_class.assert_called_once_with('container', client=self.client, headers={'properties': 'property'})
-
+
def test_get_container(self):
loaded_item = Mock()
self.container_class().load.return_value = loaded_item
result = self.client.get_container('container_name')
self.assert_(loaded_item == result)
self.container_class.assert_called_with('container_name', client=self.client, headers=None)
-
+
def test_create_container(self):
created_item = Mock()
self.container_class().create.return_value = created_item
result = self.client.create_container('container_name')
self.assert_(created_item == result, 'returns the container itself')
self.container_class.assert_called_with('container_name', client=self.client, headers=None)
-
+
def test_list_containers(self):
self.connection.storage_url = 'storage_url'
self.connection.make_request().content = '[{"name":"container_name","count":10,"bytes":100}]'
- containers = self.client.containers()
- #self.connection.make_request.assert_called_with('GET',
+ #containers = self.client.containers()
+ #self.connection.make_request.assert_called_with('GET',
# 'storage_url',
# headers=None,
# params={'format': 'json'})
-
+
def test_object(self):
self.client.storage_object('container', 'name')
self.object_class.assert_called_once_with('container', 'name', client=self.client, headers=None)
-
+
def test_object_with_props(self):
self.client.storage_object('container', 'name', {'properties': 'property'})
self.object_class.assert_called_once_with('container', 'name', client=self.client, headers={'properties': 'property'})
-
+
def test_get_object(self):
loaded_item = Mock()
self.object_class().load.return_value = loaded_item
result = self.client.get_object('object_name', 'container_name')
self.assert_(loaded_item == result, "Returns the correct object")
self.object_class.assert_called_with('object_name', 'container_name', client=self.client, headers=None)
-
+
def test_make_request(self):
self.connection.storage_url = 'storage_url'
self.client.make_request('METHOD', 'PATH')
self.connection.make_request.assert_called_once_with('METHOD', "storage_url/PATH")
-
+
def test_make_request_listpath(self):
self.connection.storage_url = 'storage_url'
self.client.make_request('METHOD', ['PATH', 'PATH2'])
self.connection.make_request.assert_called_once_with('METHOD', "storage_url/PATH/PATH2")
-
+
def test_is_dir(self):
self.assert_(self.client.is_dir() == True, "Client itself is a directory")
@@ -99,12 +103,13 @@ def test_get_url(self):
def test_chunk_upload(self):
_headers = Mock()
_chunkable = Mock()
+ _size = Mock()
_url = Mock()
self.client.get_url = Mock(return_value=_url)
self.connection.chunk_upload.return_value = _chunkable
- chunkable = self.client.chunk_upload('path', headers=_headers)
+ chunkable = self.client.chunk_upload('path', headers=_headers, size=_size)
self.assert_(chunkable == _chunkable, "Chunkable returns from conn.get_chunkable")
- self.connection.chunk_upload.assert_called_once_with('PUT', _url, _headers)
+ self.connection.chunk_upload.assert_called_once_with('PUT', _url, headers=_headers, size=_size)
def test_getitem(self):
_container = Mock()
@@ -118,8 +123,11 @@ def setUp(self):
self.container_class = Mock()
self.object_class = Mock()
self.connection_class = Mock()
- self.client = Client( 'username', 'api_key',
+ self.client = Client('username', 'api_key',
container_class=self.container_class,
object_class=self.object_class,
connection=self.connection,
)
+
+if __name__ == "__main__":
+ unittest.main()
View
15 tests/unit/container.py → tests/test_container.py 100755 → 100644
@@ -1,8 +1,12 @@
-import unittest
-from mock import Mock, MagicMock
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+from mock import Mock
from object_storage.container import Container
from object_storage.storage_object import StorageObject
+
class ContainerTest(unittest.TestCase):
def test_instance_setup(self):
self.assert_(self.client == self.container.client, "client is set")
@@ -26,9 +30,9 @@ def test_delete_recursive(self):
def test_delete_all_objects(self):
_item1 = Mock()
_item2 = Mock()
- self.container.list = Mock(return_value=[_item1, _item2])
+ self.container.objects = Mock(return_value=[_item1, _item2])
self.container.delete_all_objects()
- self.container.list.called_once_with()
+ self.container.objects.called_once_with()
_item1.delete.assert_called_once_with()
_item2.delete.assert_called_once_with()
@@ -94,3 +98,6 @@ def test_getitem(self):
def setUp(self):