8000 Fix #164: Map HTTP error responses for storage onto specific exception classes. by tseaver · Pull Request #355 · googleapis/google-cloud-python · GitHub
[go: up one dir, main page]

Skip to content

Fix #164: Map HTTP error responses for storage onto specific exception classes. #355

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

Merged
merged 2 commits into from
Nov 7, 2014
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
20 changes: 10 additions & 10 deletions gcloud/storage/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,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 @@ -176,7 +176,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 @@ -186,9 +186,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 @@ -197,7 +197,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 @@ -210,7 +210,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 @@ -219,7 +219,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 @@ -239,16 +239,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
< 8000 tr data-hunk="c54c770d085fc488182c4fc1442d619a76a5710b0ffa7b5aae7b4559fc88814e" class="show-top-border">
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
9E88
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 @@ -203,7 +203,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 @@ -215,7 +215,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 @@ -229,7 +229,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 @@ -242,7 +242,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