Skip to content

Commit

Permalink
Merge pull request #355 from tseaver/164-add_http_specific_exceptions
Browse files Browse the repository at this point in the history
Fix #164:  Map HTTP error responses for storage onto specific exception classes.
  • Loading branch information
tseaver committed Nov 7, 2014
2 parents 71e937b + e322609 commit efae0ef
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 81 deletions.
20 changes: 10 additions & 10 deletions gcloud/storage/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def get_key(self, key):
try:
response = self.connection.api_request(method='GET', path=key.path)
return Key.from_dict(response, bucket=self)
except exceptions.NotFoundError:
except exceptions.NotFound:
return None

def get_all_keys(self):
Expand Down Expand Up @@ -177,7 +177,7 @@ def delete(self, force=False):
The bucket **must** be empty in order to delete it. If the
bucket doesn't exist, this will raise a
:class:`gcloud.storage.exceptions.NotFoundError`. If the bucket
:class:`gcloud.storage.exceptions.NotFound`. If the bucket
is not empty, this will raise an Exception.
If you want to delete a non-empty bucket you can pass in a force
Expand All @@ -187,9 +187,9 @@ def delete(self, force=False):
:type force: bool
:param full: If True, empties the bucket's objects then deletes it.
:raises: :class:`gcloud.storage.exceptions.NotFoundError` if the
:raises: :class:`gcloud.storage.exceptions.NotFound` if the
bucket does not exist, or
:class:`gcloud.storage.exceptions.ConnectionError` if the
:class:`gcloud.storage.exceptions.Conflict` if the
bucket has keys and `force` is not passed.
"""
return self.connection.delete_bucket(self.name, force=force)
Expand All @@ -198,7 +198,7 @@ def delete_key(self, key):
"""Deletes a key from the current bucket.
If the key isn't found,
this will throw a :class:`gcloud.storage.exceptions.NotFoundError`.
this will throw a :class:`gcloud.storage.exceptions.NotFound`.
For example::
Expand All @@ -211,7 +211,7 @@ def delete_key(self, key):
>>> bucket.delete_key('my-file.txt')
>>> try:
... bucket.delete_key('doesnt-exist')
... except exceptions.NotFoundError:
... except exceptions.NotFound:
... pass
Expand All @@ -220,7 +220,7 @@ def delete_key(self, key):
:rtype: :class:`gcloud.storage.key.Key`
:returns: The key that was just deleted.
:raises: :class:`gcloud.storage.exceptions.NotFoundError` (to suppress
:raises: :class:`gcloud.storage.exceptions.NotFound` (to suppress
the exception, call ``delete_keys``, passing a no-op
``on_error`` callback, e.g.::
Expand All @@ -240,16 +240,16 @@ def delete_keys(self, keys, on_error=None):
:type on_error: a callable taking (key)
:param on_error: If not ``None``, called once for each key raising
:class:`gcloud.storage.exceptions.NotFoundError`;
:class:`gcloud.storage.exceptions.NotFound`;
otherwise, the exception is propagated.
:raises: :class:`gcloud.storage.exceptions.NotFoundError` (if
:raises: :class:`gcloud.storage.exceptions.NotFound` (if
`on_error` is not passed).
"""
for key in keys:
try:
self.delete_key(key)
except exceptions.NotFoundError:
except exceptions.NotFound:
if on_error is not None:
on_error(key)
else:
Expand Down
24 changes: 11 additions & 13 deletions gcloud/storage/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,8 @@ def api_request(self, method, path, query_params=None,
response, content = self.make_request(
method=method, url=url, data=data, content_type=content_type)

if response.status == 404:
raise exceptions.NotFoundError(response)
elif not 200 <= response.status < 300:
raise exceptions.ConnectionError(response, content)
if not 200 <= response.status < 300:
raise exceptions.make_exception(response, content)

if content and expect_json:
content_type = response.get('content-type', '')
Expand Down Expand Up @@ -270,7 +268,7 @@ def get_bucket(self, bucket_name):
"""Get a bucket by name.
If the bucket isn't found, this will raise a
:class:`gcloud.storage.exceptions.NotFoundError`. If you would
:class:`gcloud.storage.exceptions.NotFound`. If you would
rather get a bucket by name, and return ``None`` if the bucket
isn't found (like ``{}.get('...')``) then use
:func:`Connection.lookup`.
Expand All @@ -282,15 +280,15 @@ def get_bucket(self, bucket_name):
>>> connection = storage.get_connection(project, email, key_path)
>>> try:
>>> bucket = connection.get_bucket('my-bucket')
>>> except exceptions.NotFoundError:
>>> except exceptions.NotFound:
>>> print 'Sorry, that bucket does not exist!'
:type bucket_name: string
:param bucket_name: The name of the bucket to get.
:rtype: :class:`gcloud.storage.bucket.Bucket`
:returns: The bucket matching the name provided.
:raises: :class:`gcloud.storage.exceptions.NotFoundError`
:raises: :class:`gcloud.storage.exceptions.NotFound`
"""
bucket = self.new_bucket(bucket_name)
response = self.api_request(method='GET', path=bucket.path)
Expand Down Expand Up @@ -319,7 +317,7 @@ def lookup(self, bucket_name):
"""
try:
return self.get_bucket(bucket_name)
except exceptions.NotFoundError:
except exceptions.NotFound:
return None

def create_bucket(self, bucket):
Expand All @@ -338,7 +336,7 @@ def create_bucket(self, bucket):
:rtype: :class:`gcloud.storage.bucket.Bucket`
:returns: The newly created bucket.
:raises: :class:`gcloud.storage.exceptions.ConnectionError` if
:raises: :class:`gcloud.storage.exceptions.Conflict` if
there is a confict (bucket already exists, invalid name, etc.)
"""
bucket = self.new_bucket(bucket)
Expand All @@ -364,12 +362,12 @@ def delete_bucket(self, bucket, force=False):
True
If the bucket doesn't exist, this will raise a
:class:`gcloud.storage.exceptions.NotFoundError`::
:class:`gcloud.storage.exceptions.NotFound`::
>>> from gcloud.storage import exceptions
>>> try:
>>> connection.delete_bucket('my-bucket')
>>> except exceptions.NotFoundError:
>>> except exceptions.NotFound:
>>> print 'That bucket does not exist!'
:type bucket: string or :class:`gcloud.storage.bucket.Bucket`
Expand All @@ -380,9 +378,9 @@ def delete_bucket(self, bucket, force=False):
:rtype: bool
:returns: True if the bucket was deleted.
:raises: :class:`gcloud.storage.exceptions.NotFoundError` if the
:raises: :class:`gcloud.storage.exceptions.NotFound` if the
bucket doesn't exist, or
:class:`gcloud.storage.exceptions.ConnectionError` if the
:class:`gcloud.storage.exceptions.Conflict` if the
bucket has keys and `force` is not passed.
"""
bucket = self.new_bucket(bucket)
Expand Down
181 changes: 168 additions & 13 deletions gcloud/storage/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,179 @@
"""Custom exceptions for gcloud.storage package."""
"""Custom exceptions for gcloud.storage package.
See: https://cloud.google.com/storage/docs/json_api/v1/status-codes
"""

import json

_HTTP_CODE_TO_EXCEPTION = {} # populated at end of module


class StorageError(Exception):
"""Base error class for gcloud errors."""
"""Base error class for gcloud errors (abstract).
Each subclass represents a single type of HTTP error response.
"""
code = None
"""HTTP status code. Concrete subclasses *must* define.
class ConnectionError(StorageError):
"""Exception corresponding to a bad HTTP/RPC connection."""
See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
"""

def __init__(self, response, content):
message = str(response) + content
super(ConnectionError, self).__init__(message)
def __init__(self, message, errors=()):
super(StorageError, self).__init__()
# suppress deprecation warning under 2.6.x
self.message = message
self._errors = [error.copy() for error in errors]

def __str__(self):
return '%d %s' % (self.code, self.message)

class NotFoundError(StorageError):
"""Exception corresponding to a 404 not found bad connection."""
@property
def errors(self):
"""Detailed error information.
def __init__(self, response):
super(NotFoundError, self).__init__('')
# suppress deprecation warning under 2.6.x
self.message = 'Request returned a 404. Headers: %s' % (response,)
:rtype: list(dict)
:returns: a list of mappings describing each error.
"""
return [error.copy() for error in self._errors]


class Redirection(StorageError):
"""Base for 3xx responses
This class is abstract.
"""


class MovedPermanently(Redirection):
"""Exception mapping a '301 Moved Permanently' response."""
code = 301


class NotModified(Redirection):
"""Exception mapping a '304 Not Modified' response."""
code = 304


class TemporaryRedirect(Redirection):
"""Exception mapping a '307 Temporary Redirect' response."""
code = 307


class ResumeIncomplete(Redirection):
"""Exception mapping a '308 Resume Incomplete' response."""
code = 308


class ClientError(StorageError):
"""Base for 4xx responses
This class is abstract
"""


class BadRequest(ClientError):
"""Exception mapping a '400 Bad Request' response."""
code = 400


class Unauthorized(ClientError):
"""Exception mapping a '401 Unauthorized' response."""
code = 400


class Forbidden(ClientError):
"""Exception mapping a '403 Forbidden' response."""
code = 400


class NotFound(ClientError):
"""Exception mapping a '404 Not Found' response."""
code = 404


class MethodNotAllowed(ClientError):
"""Exception mapping a '405 Method Not Allowed' response."""
code = 405


class Conflict(ClientError):
"""Exception mapping a '409 Conflict' response."""
code = 409


class LengthRequired(ClientError):
"""Exception mapping a '411 Length Required' response."""
code = 411


class PreconditionFailed(ClientError):
"""Exception mapping a '412 Precondition Failed' response."""
code = 412


class RequestRangeNotSatisfiable(ClientError):
"""Exception mapping a '416 Request Range Not Satisfiable' response."""
code = 416


class TooManyRequests(ClientError):
"""Exception mapping a '429 Too Many Requests' response."""
code = 429


class ServerError(StorageError):
"""Base for 5xx responses: (abstract)"""


class InternalServerError(ServerError):
"""Exception mapping a '500 Internal Server Error' response."""
code = 500


class NotImplemented(ServerError):
"""Exception mapping a '501 Not Implemented' response."""
code = 501


class ServiceUnavailable(ServerError):
"""Exception mapping a '503 Service Unavailable' response."""
code = 503


def make_exception(response, content):
"""Factory: create exception based on HTTP response code.
:rtype: instance of :class:`StorageError`, or a concrete subclass.
"""

if isinstance(content, str):
content = json.loads(content)

message = content.get('message')
error = content.get('error', {})
errors = error.get('errors', ())

try:
klass = _HTTP_CODE_TO_EXCEPTION[response.status]
except KeyError:
error = StorageError(message, errors)
error.code = response.status
else:
error = klass(message, errors)
return error


def _walk_subclasses(klass):
"""Recursively walk subclass tree."""
for sub in klass.__subclasses__():
yield sub
for subsub in _walk_subclasses(sub):
yield subsub


# Build the code->exception class mapping.
for eklass in _walk_subclasses(StorageError):
code = getattr(eklass, 'code', None)
if code is not None:
_HTTP_CODE_TO_EXCEPTION[code] = eklass
8 changes: 4 additions & 4 deletions gcloud/storage/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def delete(self):
:rtype: :class:`Key`
:returns: The key that was just deleted.
:raises: :class:`gcloud.storage.exceptions.NotFoundError`
:raises: :class:`gcloud.storage.exceptions.NotFound`
(propagated from
:meth:`gcloud.storage.bucket.Bucket.delete_key`).
"""
Expand All @@ -202,7 +202,7 @@ def download_to_file(self, file_obj):
:type file_obj: file
:param file_obj: A file handle to which to write the key's data.
:raises: :class:`gcloud.storage.exceptions.NotFoundError`
:raises: :class:`gcloud.storage.exceptions.NotFound`
"""
for chunk in _KeyDataIterator(self):
file_obj.write(chunk)
Expand All @@ -216,7 +216,7 @@ def download_to_filename(self, filename):
:type filename: string
:param filename: A filename to be passed to ``open``.
:raises: :class:`gcloud.storage.exceptions.NotFoundError`
:raises: :class:`gcloud.storage.exceptions.NotFound`
"""
with open(filename, 'wb') as file_obj:
self.download_to_file(file_obj)
Expand All @@ -229,7 +229,7 @@ def download_as_string(self):
:rtype: string
:returns: The data stored in this key.
:raises: :class:`gcloud.storage.exceptions.NotFoundError`
:raises: :class:`gcloud.storage.exceptions.NotFound`
"""
string_buffer = StringIO()
self.download_to_file(string_buffer)
Expand Down
Loading

0 comments on commit efae0ef

Please sign in to comment.