Skip to content

Commit 89788fb

Browse files
committed
Map HTTP error responses for storage onto specific exception classes.
Fixes #164.
1 parent 8cd4c5e commit 89788fb

File tree

9 files changed

+263
-81
lines changed

9 files changed

+263
-81
lines changed

gcloud/storage/bucket.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def get_key(self, key):
126126
try:
127127
response = self.connection.api_request(method='GET', path=key.path)
128128
return Key.from_dict(response, bucket=self)
129-
except exceptions.NotFoundError:
129+
except exceptions.NotFound:
130130
return None
131131

132132
def get_all_keys(self):
@@ -176,7 +176,7 @@ def delete(self, force=False):
176176
177177
The bucket **must** be empty in order to delete it. If the
178178
bucket doesn't exist, this will raise a
179-
:class:`gcloud.storage.exceptions.NotFoundError`. If the bucket
179+
:class:`gcloud.storage.exceptions.NotFound`. If the bucket
180180
is not empty, this will raise an Exception.
181181
182182
If you want to delete a non-empty bucket you can pass in a force
@@ -186,9 +186,9 @@ def delete(self, force=False):
186186
:type force: bool
187187
:param full: If True, empties the bucket's objects then deletes it.
188188
189-
:raises: :class:`gcloud.storage.exceptions.NotFoundError` if the
189+
:raises: :class:`gcloud.storage.exceptions.NotFound` if the
190190
bucket does not exist, or
191-
:class:`gcloud.storage.exceptions.ConnectionError` if the
191+
:class:`gcloud.storage.exceptions.Conflict` if the
192192
bucket has keys and `force` is not passed.
193193
"""
194194
return self.connection.delete_bucket(self.name, force=force)
@@ -197,7 +197,7 @@ def delete_key(self, key):
197197
"""Deletes a key from the current bucket.
198198
199199
If the key isn't found,
200-
this will throw a :class:`gcloud.storage.exceptions.NotFoundError`.
200+
this will throw a :class:`gcloud.storage.exceptions.NotFound`.
201201
202202
For example::
203203
@@ -210,7 +210,7 @@ def delete_key(self, key):
210210
>>> bucket.delete_key('my-file.txt')
211211
>>> try:
212212
... bucket.delete_key('doesnt-exist')
213-
... except exceptions.NotFoundError:
213+
... except exceptions.NotFound:
214214
... pass
215215
216216
@@ -219,7 +219,7 @@ def delete_key(self, key):
219219
220220
:rtype: :class:`gcloud.storage.key.Key`
221221
:returns: The key that was just deleted.
222-
:raises: :class:`gcloud.storage.exceptions.NotFoundError` (to suppress
222+
:raises: :class:`gcloud.storage.exceptions.NotFound` (to suppress
223223
the exception, call ``delete_keys``, passing a no-op
224224
``on_error`` callback, e.g.::
225225
@@ -239,16 +239,16 @@ def delete_keys(self, keys, on_error=None):
239239
240240
:type on_error: a callable taking (key)
241241
:param on_error: If not ``None``, called once for each key raising
242-
:class:`gcloud.storage.exceptions.NotFoundError`;
242+
:class:`gcloud.storage.exceptions.NotFound`;
243243
otherwise, the exception is propagated.
244244
245-
:raises: :class:`gcloud.storage.exceptions.NotFoundError` (if
245+
:raises: :class:`gcloud.storage.exceptions.NotFound` (if
246246
`on_error` is not passed).
247247
"""
248248
for key in keys:
249249
try:
250250
self.delete_key(key)
251-
except exceptions.NotFoundError:
251+
except exceptions.NotFound:
252252
if on_error is not None:
253253
on_error(key)
254254
else:

gcloud/storage/connection.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,8 @@ def api_request(self, method, path, query_params=None,
231231
response, content = self.make_request(
232232
method=method, url=url, data=data, content_type=content_type)
233233

234-
if response.status == 404:
235-
raise exceptions.NotFoundError(response)
236-
elif not 200 <= response.status < 300:
237-
raise exceptions.ConnectionError(response, content)
234+
if not 200 <= response.status < 300:
235+
raise exceptions.make_exception(response, content)
238236

239237
if content and expect_json:
240238
content_type = response.get('content-type', '')
@@ -270,7 +268,7 @@ def get_bucket(self, bucket_name):
270268
"""Get a bucket by name.
271269
272270
If the bucket isn't found, this will raise a
273-
:class:`gcloud.storage.exceptions.NotFoundError`. If you would
271+
:class:`gcloud.storage.exceptions.NotFound`. If you would
274272
rather get a bucket by name, and return ``None`` if the bucket
275273
isn't found (like ``{}.get('...')``) then use
276274
:func:`Connection.lookup`.
@@ -282,15 +280,15 @@ def get_bucket(self, bucket_name):
282280
>>> connection = storage.get_connection(project, email, key_path)
283281
>>> try:
284282
>>> bucket = connection.get_bucket('my-bucket')
285-
>>> except exceptions.NotFoundError:
283+
>>> except exceptions.NotFound:
286284
>>> print 'Sorry, that bucket does not exist!'
287285
288286
:type bucket_name: string
289287
:param bucket_name: The name of the bucket to get.
290288
291289
:rtype: :class:`gcloud.storage.bucket.Bucket`
292290
:returns: The bucket matching the name provided.
293-
:raises: :class:`gcloud.storage.exceptions.NotFoundError`
291+
:raises: :class:`gcloud.storage.exceptions.NotFound`
294292
"""
295293
bucket = self.new_bucket(bucket_name)
296294
response = self.api_request(method='GET', path=bucket.path)
@@ -319,7 +317,7 @@ def lookup(self, bucket_name):
319317
"""
320318
try:
321319
return self.get_bucket(bucket_name)
322-
except exceptions.NotFoundError:
320+
except exceptions.NotFound:
323321
return None
324322

325323
def create_bucket(self, bucket):
@@ -338,7 +336,7 @@ def create_bucket(self, bucket):
338336
339337
:rtype: :class:`gcloud.storage.bucket.Bucket`
340338
:returns: The newly created bucket.
341-
:raises: :class:`gcloud.storage.exceptions.ConnectionError` if
339+
:raises: :class:`gcloud.storage.exceptions.Conflict` if
342340
there is a confict (bucket already exists, invalid name, etc.)
343341
"""
344342
bucket = self.new_bucket(bucket)
@@ -364,12 +362,12 @@ def delete_bucket(self, bucket, force=False):
364362
True
365363
366364
If the bucket doesn't exist, this will raise a
367-
:class:`gcloud.storage.exceptions.NotFoundError`::
365+
:class:`gcloud.storage.exceptions.NotFound`::
368366
369367
>>> from gcloud.storage import exceptions
370368
>>> try:
371369
>>> connection.delete_bucket('my-bucket')
372-
>>> except exceptions.NotFoundError:
370+
>>> except exceptions.NotFound:
373371
>>> print 'That bucket does not exist!'
374372
375373
:type bucket: string or :class:`gcloud.storage.bucket.Bucket`
@@ -380,9 +378,9 @@ def delete_bucket(self, bucket, force=False):
380378
381379
:rtype: bool
382380
:returns: True if the bucket was deleted.
383-
:raises: :class:`gcloud.storage.exceptions.NotFoundError` if the
381+
:raises: :class:`gcloud.storage.exceptions.NotFound` if the
384382
bucket doesn't exist, or
385-
:class:`gcloud.storage.exceptions.ConnectionError` if the
383+
:class:`gcloud.storage.exceptions.Conflict` if the
386384
bucket has keys and `force` is not passed.
387385
"""
388386
bucket = self.new_bucket(bucket)

gcloud/storage/exceptions.py

Lines changed: 159 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,170 @@
1-
"""Custom exceptions for gcloud.storage package."""
1+
"""Custom exceptions for gcloud.storage package.
2+
3+
See: https://cloud.google.com/storage/docs/json_api/v1/status-codes
4+
"""
5+
6+
import json
7+
8+
_HTTP_CODE_TO_EXCEPTION = {} # populated at end of module
29

310

411
class StorageError(Exception):
5-
"""Base error class for gcloud errors."""
12+
"""Base error class for gcloud errors (abstract).
613
14+
Each subclass represents a single type of HTTP error response.
15+
"""
16+
code = None
17+
"""HTTP status code. Concrete subclasses *must* define.
718
8-
class ConnectionError(StorageError):
9-
"""Exception corresponding to a bad HTTP/RPC connection."""
19+
See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
20+
"""
1021

11-
def __init__(self, response, content):
12-
message = str(response) + content
13-
super(ConnectionError, self).__init__(message)
22+
def __init__(self, message, errors=()):
23+
super(StorageError, self).__init__()
1424
# suppress deprecation warning under 2.6.x
1525
self.message = message
26+
self._errors = [error.copy() for error in errors]
1627

28+
def __str__(self):
29+
return '%d %s' % (self.code, self.message)
1730

18-
class NotFoundError(StorageError):
19-
"""Exception corresponding to a 404 not found bad connection."""
31+
@property
32+
def errors(self):
33+
"""Detailed error information.
2034
21-
def __init__(self, response):
22-
super(NotFoundError, self).__init__('')
23-
# suppress deprecation warning under 2.6.x
24-
self.message = 'Request returned a 404. Headers: %s' % (response,)
35+
:rtype: list(dict)
36+
:returns: a list of mappings describing each error.
37+
"""
38+
return [error.copy() for error in self._errors]
39+
40+
41+
class Redirection(StorageError):
42+
"""Base for 3xx responses
43+
44+
This class is abstract.
45+
"""
46+
47+
48+
class MovedPermanently(Redirection):
49+
"""Exception mapping a '301 Moved Permanently' response."""
50+
code = 301
51+
52+
53+
class NotModified(Redirection):
54+
"""Exception mapping a '304 Not Modified' response."""
55+
code = 304
56+
57+
58+
class TemporaryRedirect(Redirection):
59+
"""Exception mapping a '307 Temporary Redirect' response."""
60+
code = 307
61+
62+
63+
class ResumeIncomplete(Redirection):
64+
"""Exception mapping a '308 Resume Incomplete' response."""
65+
code = 308
66+
67+
68+
class ClientError(StorageError):
69+
"""Base for 4xx responses
70+
71+
This class is abstract
72+
"""
73+
74+
75+
class BadRequest(ClientError):
76+
"""Exception mapping a '400 Bad Request' response."""
77+
code = 400
78+
79+
80+
class Unauthorized(ClientError):
81+
"""Exception mapping a '401 Unauthorized' response."""
82+
code = 400
83+
84+
85+
class Forbidden(ClientError):
86+
"""Exception mapping a '403 Forbidden' response."""
87+
code = 400
88+
89+
90+
class NotFound(ClientError):
91+
"""Exception mapping a '404 Not Found' response."""
92+
code = 404
93+
94+
95+
class MethodNotAllowed(ClientError):
96+
"""Exception mapping a '405 Method Not Allowed' response."""
97+
code = 405
98+
99+
100+
class Conflict(ClientError):
101+
"""Exception mapping a '409 Conflict' response."""
102+
code = 409
103+
104+
105+
class LengthRequired(ClientError):
106+
"""Exception mapping a '411 Length Required' response."""
107+
code = 411
108+
109+
110+
class PreconditionFailed(ClientError):
111+
"""Exception mapping a '412 Precondition Failed' response."""
112+
code = 412
113+
114+
115+
class RequestRangeNotSatisfiable(ClientError):
116+
"""Exception mapping a '416 Request Range Not Satisfiable' response."""
117+
code = 416
118+
119+
120+
class TooManyRequests(ClientError):
121+
"""Exception mapping a '429 Too Many Requests' response."""
122+
code = 429
123+
124+
125+
class ServerError(StorageError):
126+
"""Base for 5xx responses: (abstract)"""
127+
128+
129+
class InternalServerError(ServerError):
130+
"""Exception mapping a '500 Internal Server Error' response."""
131+
code = 500
132+
133+
134+
class NotImplemented(ServerError):
135+
"""Exception mapping a '501 Not Implemented' response."""
136+
code = 501
137+
138+
139+
class ServiceUnavailable(ServerError):
140+
"""Exception mapping a '503 Service Unavailable' response."""
141+
code = 503
142+
143+
144+
def make_exception(response, content):
145+
"""Factory: create exception based on HTTP response code.
146+
147+
:rtype: instance of :class:`StorageError`, or a concrete subclass.
148+
"""
149+
150+
if isinstance(content, str):
151+
content = json.loads(content)
152+
153+
message = content.get('message')
154+
error = content.get('error', {})
155+
errors = error.get('errors', ())
156+
157+
try:
158+
klass = _HTTP_CODE_TO_EXCEPTION[response.status]
159+
except KeyError:
160+
error = StorageError(message, errors)
161+
error.code = response.status
162+
else:
163+
error = klass(message, errors)
164+
return error
165+
166+
167+
for name, value in globals().items():
168+
code = getattr(value, 'code', None)
169+
if code is not None:
170+
_HTTP_CODE_TO_EXCEPTION[code] = value

gcloud/storage/key.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def delete(self):
203203
204204
:rtype: :class:`Key`
205205
:returns: The key that was just deleted.
206-
:raises: :class:`gcloud.storage.exceptions.NotFoundError`
206+
:raises: :class:`gcloud.storage.exceptions.NotFound`
207207
(propagated from
208208
:meth:`gcloud.storage.bucket.Bucket.delete_key`).
209209
"""
@@ -215,7 +215,7 @@ def download_to_file(self, file_obj):
215215
:type file_obj: file
216216
:param file_obj: A file handle to which to write the key's data.
217217
218-
:raises: :class:`gcloud.storage.exceptions.NotFoundError`
218+
:raises: :class:`gcloud.storage.exceptions.NotFound`
219219
"""
220220
for chunk in _KeyDataIterator(self):
221221
file_obj.write(chunk)
@@ -229,7 +229,7 @@ def download_to_filename(self, filename):
229229
:type filename: string
230230
:param filename: A filename to be passed to ``open``.
231231
232-
:raises: :class:`gcloud.storage.exceptions.NotFoundError`
232+
:raises: :class:`gcloud.storage.exceptions.NotFound`
233233
"""
234234
with open(filename, 'wb') as file_obj:
235235
self.download_to_file(file_obj)
@@ -242,7 +242,7 @@ def download_as_string(self):
242242
243243
:rtype: string
244244
:returns: The data stored in this key.
245-
:raises: :class:`gcloud.storage.exceptions.NotFoundError`
245+
:raises: :class:`gcloud.storage.exceptions.NotFound`
246246
"""
247247
string_buffer = StringIO()
248248
self.download_to_file(string_buffer)

gcloud/storage/test_acl.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,12 +697,12 @@ def __init__(self, *responses):
697697
self._deleted = []
698698

699699
def api_request(self, **kw):
700-
from gcloud.storage.exceptions import NotFoundError
700+
from gcloud.storage.exceptions import NotFound
701701
self._requested.append(kw)
702702

703703
try:
704704
response, self._responses = self._responses[0], self._responses[1:]
705705
except: # pragma: NO COVER
706-
raise NotFoundError('miss')
706+
raise NotFound('miss')
707707
else:
708708
return response

0 commit comments

Comments
 (0)